记一次React Hooks的使用

有这么一个需求:需要渲染一个表格,表格的内容会随着用户的操作而重新请求数据,并且在用户离开这个表格所在的页面(路由)后缓存数据,再次进入该页面的时使用已缓存数据。

目前在使用的表格组件是纯函数组件,只负责渲染,数据请求则写在其父组件,数组则存在Redux中。经过考虑,需要把数据请求的逻辑移入表格组件中,使得表格组件承担更多的职责,在React 16.8之前,我们不得不把表格组件写成class组件。

而现在,可以使用hooks,以最少的更改,来实现这一需求。

需要哪些hooks?

我们的需求是把请求数据的逻辑移入到表格组件中,表格的数据仍然保存在Redux中。众所周知,数据获取是一个有副作用的操作,而useEffect这个hooks就是用来处理有副作用的操作。

在使用hooks之前,我们一般在componentDidMountcomponentDidUpdate或者是很少使用的componentWillUnMount来进行DOM操作,数据请求等副作用操作。而useEffect则可以简单的看做是这三个生命周期函数的合集,其在组件的这三个生命周期时,都会被调用到。

所以,使用useEffect解决了所有问题。

“真”解决了所有问题?

先把代码写起来:

1
2
3
4
5
6
7
8
9
10
import React, { useEffect } from 'react';
const MyTable = ({ fetchData, param }) => {
useEffect(() => {
fetchData(param);
}, [param])
return (
<Table />
);
}
export default connect(stateToProps,actionToProps)(MyTable);

仅仅三行代码,就搞定了。更改params,一切正常;从其它页面进入,看起来也很正常。可为什么是看起来正常呢?因为你打开控制台,查看network xhr,再进入页面,请求发送了!并没有使用缓存的数据!

所以,问题并没有解决。

如何更好的利用缓存数据

问题:既然useEffect能够在上述三个生命周期中都执行,那么有没有办法区分出首次渲染和更新呢?

答案是肯定的!在上面的代码中,我们使用了useEffect(func, [param])的形式,其实useEffect的第二个参数如果指定,那么useEffect就不是每次都执行了,而是只有param改变了才会执行。并且特别的,如果第二个参数传入[]空数组,那么useEffect只会执行一次,也就是说,useEffect只会在componentDidMount执行!代码写起来:

1
2
3
4
5
6
// 其它代码不变,再加一个hook
useEffect(() => {
if (data.status !== 'fulfilled') {
fetchData(param);
}
}, [])

上面的hook会在初次渲染完成后执行,如果缓存数据的状态不是fulfilled,才请求数据。

问题仍然没有解决

很显而易见的是,即便是加了一个hook,而第二个useEffect仍然会执行,所以仍然会在初次加载完成后请求数据。

这时候,就需要另一个hook出场了,那就是useState,我们需要在组件中维持一个是否是首次渲染的状态,只有当非首次渲染的时候,才会去执行第一个hook,因此可以避免不必要的的数据请求。代码码起来:

1
2
3
4
5
6
7
8
9
10
11
12
const [isInitial, changeInitialToFalse] = useState(true);
useEffect(() => {
if (!isInitial) {
fetchData(param);
}
}, [param]);
useEffect(() => {
changeInitialToFalse(false);
if (data.status !== 'fulfilled') {
fetchData(param);
}
}, []);

经过测试,这一次是真一切正常了,缓存也已经用上。

尾巴

这是一次再普通不过的组件改造,这也是我在实际项目中第一次使用hooks。并且经历了从最开始遇到需求到决定使用hooks来最小化修改,到遇到问题差点改成更熟悉的class组件,到最终解决问题。最大的收货是,我对useEffect的了解又深刻了一些。把数据请求逻辑移到组件内部,除了降低组件间的耦合,更大程度上可以配合前一篇提到的ScrollLoad来做真正的滚动加载。毕竟数据才是组件的灵魂,数据都不懒加载,组件懒加载的意义就减了一半,手动狗头。

完。

分享到 评论

Webpack resolve解析

虽然在实际开发过程中,我们经常会使用脚手架来初始化一个项目,而脚手架一般都包含了完善的webpack配置,例如create-react-app这个脚手架,其把所有关于构建的内容封装在了react-scripts包中,在实际开发中,我们只需要运行yarn run start/build/test即可,它就可以帮我们搞定代码压缩、分隔,jsxes6代码编译到es5等。

回到webpack,我们都知道一个完整的webpack配置必定是要包含入口(entry)输出(output),可能还需要模块-加载器(loader)来处理不同类型的模块、或是使用插件(plugin)来在构建过程中自定义某些动作。除此之外,还有webpack4中才引入的用于性能和构建优化的optimization,还有用于开发环境的开发服务器(devServer)。还有不那么常用和深入人心的解析(resolve)。本篇将以react-scripts包的webpack配置中关于resolve的使用为基础,介绍如何在实际项目中可能会用到的自定义解析。

他们是怎么写的

首先来看看react-scripts关于resolve的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
resolve: {
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
extensions: paths.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: {
'react-native': 'react-native-web',
},
plugins: [
PnpWebpackPlugin,
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
],
},

  1. modules属性:其指定了webpack在进行模块解析时应该搜索的目录,该属性可通过数组的方式指定一系列的路径,默认值是: ["node_modules"],也就是说,在不指定该属性的情况下,如果我们引入import xx from xx,则webpack会默认在根目录的node_modules目录下查找该模块。上面的配置中指定了额外两种模块解析路径,其分别是path.appNodeModulesmodules.additionalModulePaths,其中path.appNodeModules最终指向的是path.resolve(fs.realpathSync(process.cwd()), 'node_modules')也就是说,在默认情况下,该路径是node_modules决定路径modules.additionalModulePaths指向一个自定义路径,并通过getAdditionalModulePaths(config)方法生成该路径,如果config等于{},则返回process.env.NODE_PATH(经过一些列的处理),否则的话如果config.baseUrl存在且等于modules.additionalModulePaths或者appSrc则返回,否则抛出错误。也就是说,我们可以通过jsconfig.json来指定baseUrl属性,并且该属性只能是node_modules目录或src目录。

  2. extensions属性:自动解析的确定的扩展,默认值是['.js', '.json'],也就是说,在默认情况下,我们import ClsA from './clsa',可以解析到clsa.js或是cls.json。上面的配置重写了extensions,定义了更多的扩展,并根据当前是否是typescript项目而是用对应的拓展。paths.moduleFileExtensions定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const moduleFileExtensions = [
    'web.mjs',
    'mjs',
    'web.js',
    'js',
    'web.ts',
    'ts',
    'web.tsx',
    'tsx',
    'json',
    'web.jsx',
    'jsx',
    ];

如何判别当前项目是typescript项目呢?
useTypeScript是这样定义的:const useTypeScript = fs.existsSync(paths.appTsConfig);,很明显,通过判断是否存在paths.appTsConfig指向的文件也就是tsconfig.json,如果存在该文件,则表示该项目是中可以引用那些以ts为后缀的文件而不需要指定扩展名。

  1. alias属性:创建模块的别名,确保在引入某些模块时可以变得简单。在上面中alias的配置是:
    1
    2
    3
    alias: {
    'react-native': 'react-native-web',
    },

也就是在我们使用名为react-native的模块时,其默认指向的是react-native-web模块,例如:

1
import { View } from 'react-native';

此时项目中根本没安装react-native,仅安装了react-native-web,也就是说react-native成了react-native-web的别名。那么问题来了:为什么我们不直接写import { View } from 'react-native-web'呢?

其实这涉及到React-Native强调的一次编写,处处使用,这里的使用并不仅仅是iOSAndroid代码共用,而是React-NativeWeb之间的代码共享。react-native-web就是这样一个库,它把react-native实现的组件实现成为Web组件,并且表现和react-native组件一致。这样,如果我们拿到的是一份react-native的代码,添加别名过后,就无需把所有的react-native全都改成react-native-web,这样就保证了两端代码的统一。

扯远了,继续。

  1. plugins属性:如果在配置解析的过程中需要插件的话,就可以在这里指定。上面的代码使用了pnp-webpack-pluginreact-dev-utils/ModuleScopePluginpnp-webpack-plugin是为了解决require()时过多的I/O操作带来的性能消耗,pnp思想来自Yarn团队,目的是为了解决安装和引用依赖效率过低问题。其建立了一张映射表,这张表记录了依赖版本关联和依赖与依赖之间的关联以及依赖存放的位置。有了这张表,就可以跳过繁琐的查找过程直接确定依赖在文件中的位置,从而提高性能。详情见stackoverflowModuleScopePlugin插件的官方解释是:该插件可以确保来自源程序目录(也就是/src)的相对导入不会使用到外部依赖。new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson])接收两个参数,分别指定了源程序目录/srcallowedFiles指向了package.json文件。

更多的配置

  1. mainFields属性:该属性是为了指定在不同环境中,默认使用哪个webpack的字段作为导入的入口。例如某个模块的package.json文件中执行了以下入口:
    1
    2
    3
    4
    5
    {
    module: 'index.js',
    main: 'build/index.node.js',
    browser: 'build/index.js',
    }

那么在Node环境下,默认会使用build/index.node.js导入,而在浏览器环境中,默认使用build/index.js导入。

  1. mainFiles属性:解析目录是默认使用的文件名。这个在实际开发中使用得比较多,例如我们开发的某个页面,文件路径为/src/views/Home/index.js,那么在实际使用这个页面的时候直接使用import Home from './src/views/Home'即可,因为mainFiles默认的配置是:['index'],这个属性不建议自定义。

resolve还有很多属性,可以让我们充分自定义整个解析过程,但从react-scripts的实践上来看,其也是针对某些属性进行了定制,并没有一味的自定义。在对某个属性不是很熟悉并且没有过实践,建议不要盲目的修改。并且resolve的所有属性都提供了适应绝普通场景的默认值。最后,resolve使用愉快。

完。

分享到 评论

来,实现一个“滚动加载”

从问题入手,实现一个滚动加载

实现原理

懒加载的实现很简单,例如图片需要懒加载的时候,在初始加载时并不直接加载图片,而是用一个其它的图片或者占位符占位,当其它优先级更高的内容加载完成后,再使用真实的图片替换占位图。具体呢就是在初始加载时并不知道图片的src或者src指向一个占位图片,而真实图片的路径则是放在data-src这样的自定义属性中,当需要加载时,使用data-src替换src即可。

在React中,如果我们需要懒加载一个组件,实现原理也类似:当不需要加载的时候渲染占位符,当需要加载的时候再去加载真正的加载。滚动加载就是懒加载的一种特殊情况,其触发方式是滚动,只有当组件在可视区域中时,才开始加载。

实现滚动加载并不简单,我们需要考虑以下几个问题:

  1. 如何找到组件所处的滚动容器。
  2. 如何判定组件是否可见。
  3. 如何使用“更好”组件占位符。
  4. 处理性能瓶颈。

ScrollLoad实现

懒加载使用最为广泛的实现是react-lazy-load,使用起来也很方便:<LazyLoad><MyComponent /></LazyLoad>,使用提供的LazyLoad组件包裹需要懒加载的组件即可,并且它提供了多个属性,例如height可以设置当内容未加载时的高度,offset则可以指定组件开始加载时需要偏移的距离(单位:px),除此之外,我们还可以指定懒加载是否使用防抖和节流来提升性能,具体的使用见react-lazy-load

虽然react-lazy-load是一个使用广泛的懒加载解决方案,但是在最近的项目中,我却不得不放弃使用它。因为react-lazy-load会更改原有的DOM结构!!!,所以如果要使用react-lazy-load,我必须更改原本的样式,这将会是一个浩大的工程。

所以,接下来将会实现一个不需要额外DOM结构的滚动加载组件ScrollLoad组件。

问题1:如果找到组件所在的可滚动元素

这个问题其实是:如何找到组件所在的最近的可滚动的父元素

但是为什么需要找到那个可滚动的父元素呢?因为需要在该父元素上绑定scroll eventListener,当父元素在滚动时,就可以根据监听函数实时获取滚动的距离,并依此决定组件显示与否。

那怎么样才能找到最近可滚动父元素呢?为了定位父元素,我们需要定位当前元素,然后再向上遍历,去查找overflow显式设置为auto|scroll的元素。

查找可滚动父元素代码如下:

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
function getScrollParent(element) {
const style = (elem, prop) => {
if (getComputedStyle !== undefined) {
return getComputedStyle(elem, null).getPropertyValue(prop);
}
return elem.style[prop];
};
const overflow = node => style(node, 'overflow') + style(node, 'overflow-x') + style(node, 'overflow-y');
// 循环判断父节点是否可滚动这里暂不添加,直接去直接父元素
if (!(element instanceof HTMLElement)) {
return window;
}
let parent = element;
while (parent) {
// 当前节点是body或者document
if (parent === document.body || parent === document.documentElement) {
break;
}
// 当期元素无父节点
if (!parent.parentNode) {
break;
}
// 判断节点是否含有overflow等属性的值
if (/(scroll|auto|inherit)/.test(overflow(parent))) {
return parent;
}
parent = parent.parentNode;
}
return window;
}

实现参考了react-lazy-load的实现,通过当前节点向上查找直到找到第一个可滚动的父元素或遍历到顶层元素,并且每进行一次遍历,会通过正则检查当前元素的样式的overflow overflow-x overflow-y,如果为scroll|auto,则返回该父元素。

那么怎么得到当前元素呢?可以通过ReactDOM.findDOMNode(component)访问真实的DOM节点,如果有多个子节点的话,默认返回第一个。因为ScrollLoad组件并没有添加额外的DOM结构,所以通过findDOMNode(this)拿到的节点就是目标节点,也就是占位节点(因为就算组件初始状态下可见,那么也要等应用挂载后才去判断,所以首次渲染的是占位组件)。

问题2:如何判定组件是否可见

如果只考虑上下滚动的话,一个很简单的判断公式是:offsetTop < seenHeight + scrollTop,就是:组件相对于可滚动父元素的偏移 < 可滚动父元素的可视高度 + 可滚动父元素的滚动距离。上面的三个计算量中offsetTopseenHeight都是固定不变的,所以一个组件是否可见取决于父元素当前滚动的距离。seenHeight很好计算:parent.clientHeight即可,scrollTop也很简单:parent.scrollTopoffsetTop计算稍微复杂。

如何计算offsetTop?如果最近可滚动父元素是直接父元素的话,直接通过elem.offsetTop就可以得到,如果包含多层嵌套,那么offsetTop就需要每一层元素相对于父元素的offsetTop相加,直到父元素等于目标父元素。简单的实现如下:

1
2
3
4
5
6
7
8
9
10
// 该函数没有考虑节点异常的情况
let getNodeOffsetTop = (node, parent) => {
let current = node;
let offsetTop = 0;
while (current !== parent) {
offsetTop += current.offsetTop;
current = current.parentElement;
}
return offsetTop;
}

最后是判断组件是否可见的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let checkVisible = (node, parent) => {
if (!node || !parent) {
this.setState({ visible: true });
return null;
}
return () => {
const { visible } = this.state;
if (visible) {
this.parent.removeEventListener('scroll', this.scrollHandler);
return; // 直接返回不执行当次eventListener
}
let seenHeight = parent.clientHeight;
let scrollHeight = parent.scrollTop;
let currentNode = findDOMNode(this); // 获取最新的dom结构
let offsetTop = this.getNodeOffsetTop(currentNode, parent);
// 1. 当偏移高度小于可见高度
// 2. 初始不可见的时候,当可视高度+滚动高度大于了偏移高度
if (offsetTop <= seenHeight + scrollHeight) {
this.setState({ visible: true });
}
};
}

问题3:如何使用“更好”组件占位符

理想情况下,一个好的占位组件应该是和真实组件一样大的size,这样的话初始情况和加载完成的情况下滚动条的长度都是一样长的,且在组件由不可见到可见这个过程中页面并不会因为组件前后的size而出现抖动。

而实际的情况是,我们并不能很好的拿到目标组件的样式并作用到占位组件上,因为占位组件总是先渲染。所以折中的做法是让ScrollLoad的使用者去提供占位组件,这样就把如何提供一个好的占位组件交给了使用者。

还有一种办法是,既然一个好的(只是我认为的)占位组件的size是等于目标组件的,那么我直接把用于布局的样式从目标组件上拿过来不就行了!所以无论是目标组件渲染后的真实DOM上的className id style...全部拿过来,如果高度是内容撑开的话,我们就拿渲染完成的高度直接设置到占位组件上。经过尝试,这种方法的确可行,但是需要付出的代价是需要花费时间在获取目标组件的样式和样式整理上,并且代码的可读性将一定程度的降低。

所以,关于占位组件,我暂时没有好的方法。

问题4:性能瓶颈

在前面介绍react-lazy-load的时候提到过我们可以决定是否使用节流或防抖来解决性能问题。所以,在ScrollLoad上,也是用了节流来控制scroll触发的频率。代码如下:

1
2
this.scrollHandler = throttle(this.checkVisible(dom, parent), 100);
parent.addEventListener('scroll', this.scrollHandler, { passive: true });

这里使用了lodash/throttle来实现节流,这样默认情况下,scroll事件只会每隔100ms触发一次。

上面的代码在使用addEventListener绑定监听函数时还是用到了该函数的第三个参数:{ passive: true }passive的意思是消极的、被动的,如果不指定,在新版的Chrome中会有性能提示:[Violation] Added non-passive event listener to a scroll-blocking <some> event. Consider marking event handler as 'passive' to make the page more responsive.

其实在监听滚动事件是,我们可以通过event.preventDefault()来阻止浏览器的默认滚动行为,可当滚动触发时,浏览器并不知道我们的监听函数中是否阻止了默认行为,所以浏览器会等待,直到监听函数执行完,此时浏览器才会选择滚动与否。而执行监听函数往往需要时间,性能就会受到影响。

而设置{ passive: true },可以在执行监听函数之前就告诉浏览器,并没有阻止默认滚动,因此在滚动触发时,浏览器就不会等待,直接滚动。显然ScrollLoad是需要执行浏览器滚动的,因此设置{ passive: true }可以提升性能。

checkVisible函数中,有一段代码:

1
2
3
4
const { visible } = this.state;
if (visible) {
this.parent.removeEventListener('scroll', this.scrollHandler);
}

这段代码很好理解,除了在组件卸载之前需要移除listener之外,一旦当某个组件可见,那么此时就没必要再监听滚动了,所以需要移除监听函数。

上面的代码还有一个问题,如果当某两次触发监听函数组件的状态刚好从visiblefalse切换到true,此时移除了监听函数,但当次函数还会再次执行,所以在移除监听函数后,直接返回,可以避免执行下面不必要的逻辑。因此,这段代码应该是:

1
2
3
4
5
const { visible } = this.state;
if (visible) {
this.parent.removeEventListener('scroll', this.scrollHandler);
return; // 直接返回不执行当次eventListener
}

结尾

到此为止,一个基本可用的ScrollLoad组件就实现了,其实对于不同的使用场景,ScrollLoad的实现也可以略有不同,例如如果当前需要scrollload的组件是一些列表项组件,每个组件的样式外观都是一致的。这样的话,我们在写lazyload组件的时候,就可以把所有组件使用一个LazyLoad组件包裹起来,然后绑定一个监听函数,使用可显示的组件个数作为组件状态,每次滚动时监听函数根据已经滚动的高度去计算可见组件的个数,最后在渲染的时候遍历this.props.children,选择渲染组件或是占位组件即可。

LazyLoad实现代码:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import React from 'react';
import { findDOMNode } from 'react-dom';
import throttle from 'lodash/throttle';
import { Spin } from '@xx/xx-ui';
import ReactPlaceHolder from 'react-placeholder';

function getScrollParent(element) {
const style = (elem, prop) => {
if (getComputedStyle !== undefined) {
return getComputedStyle(elem, null).getPropertyValue(prop);
}
return elem.style[prop];
};
const overflow = node => style(node, 'overflow') + style(node, 'overflow-x') + style(node, 'overflow-y');
// 循环判断父节点是否可滚动这里暂不添加,直接去直接父元素
if (!(element instanceof HTMLElement)) {
return window;
}
let parent = element;
while (parent) {
// 当前节点是body或者document
if (parent === document.body || parent === document.documentElement) {
break;
}
// 当期元素无父节点
if (!parent.parentNode) {
break;
}
// 判断节点是否含有overflow等属性的值
if (/(scroll|auto|inherit)/.test(overflow(parent))) {
return parent;
}
parent = parent.parentNode;
}
return window;
}

let EmptyCompBox = ({ ...props }) => (
<div {...props}>
<Spin size="large" className="lazyload-center-spin" />
</div>
);

class ScrollLoad extends React.Component {
state = {
visible: false,
};
componentDidMount() {
let dom = findDOMNode(this); // 取得当前节点
let parent = getScrollParent(dom);
this.parent = parent;
let visible = this.checkVisible(dom, parent); // 初始化检查是否可见
visible();
this.scrollHandler = throttle(this.checkVisible(dom, parent), 100);
parent.addEventListener('scroll', this.scrollHandler, { passive: true });
}
componentWillUnmount() {
this.parent.removeEventListener('scroll', this.scrollHandler);
}
getNodeOffsetTop = (node, parent) => {
let current = node;
let offsetTop = 0;
while (current !== parent) {
offsetTop += current.offsetTop;
current = current.parentElement;
}
return offsetTop;
};
checkVisible = (node, parent) => {
if (!node || !parent) {
this.setState({ visible: true });
return null;
}
let seenHeight = parent.clientHeight;
let scrollHeight = parent.scrollTop;
return () => {
const { visible } = this.state;
if (visible) {
this.parent.removeEventListener('scroll', this.scrollHandler);
return; // 直接返回不执行当次eventListener
}
let currentNode = findDOMNode(this); // 获取最新的dom结构
let offsetTop = this.getNodeOffsetTop(currentNode, parent);
// 1. 当偏移高度小于可见高度
// 2. 初始不可见的时候,当可视高度+滚动高度大于了偏移高度
if (offsetTop <= seenHeight + scrollHeight) {
this.setState({ visible: true });
}
};
};
render() {
const { visible } = this.state;
const { id, className, style } = this.props;
return (
<ReactPlaceHolder
ready={visible}
customPlaceholder={<EmptyCompBox id={id} className={className} style={style} />}
>
{this.props.children}
</ReactPlaceHolder>
);
}
}

export default ScrollLoad;

分享到 评论

关于eject需要知道的

整理:使用create-react-app构建的项目在yarn run eject后的一些问题。

eject之前

create-react-app脚手架把关于webpack配置和其它脚本封装到了一个叫做react-scriptspackage里。默认情况下所有的配置是不可见的,但是可以通过react-app-rewired在不eject的条件下修改某些webpack配置。这样的方式在绝大多数情况下是可行的,但是如果我们想更精确的控制,比如修改默认的css打包方式、使用less、组件库按需加载等,即使也能够做,但或多或少会用到hack的方式。

在运行yarn run eject后,所有关于webpack配置将会暴露出来,在根目录会生成configscripts两个目录,并且react-script将不复存在,所有react-script的依赖都将注入到项目中。

eject是单项操作,一旦完成,就没办法还原,这就意味着复杂的webpack管理将交由我们自己管理。

eject进行时

  1. 步骤1 运行yarn run eject,稍等片刻,观察到生成新增了configscripts两个目录,并且package.json文件被修改。

  2. 打开package.json文件,由于我们已经不使用react-scripts了,所以这里需要修改start build test等命令。使用scripts目录中对应的脚本即可,例如:

    1
    2
    3
    4
    5
    6
    "scripts": {
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "test": "node scriprs/test.js"
    ...
    }
  3. 运行yarn start,不出意外的话,项目正常运行,如果提示can’t find module xxx,删除node_modulesyarn.lock(如果有的话),重新运行yarn install安装依赖即可。

eject后出现了问题

如果是eject后运行项目报错,重新安装依赖后仍然报错,那么就要考虑是依赖版本的问题了。就比如笔者的这个项目,使用老版本的react-react-app生成,并且在中期升级了react-scriptsreact等,且使用了特定版本的eslint规则。在根据报错提示进行修复后,运行项目仍然报错且无任何报错提示。

我们需要弄清楚到底是什么依赖有问题,这个过程是复杂且繁琐的,并且可能由于依赖前后版本已经发生了巨大的变化,即使找到存在问题的依赖也可能不能仅通过升级该依赖的版本解决问题。

所以一个备选方案是把项目整体迁移到新版本的create-react-app上,这里有两个做法:使用create-react-app新生成一个项目,我们把源码和依赖都迁移到新项目中,在新的项目中进行eject;使用create-react-app生成一个空的项目并eject,使用新的configscripts替换当前项目的脚本和配置,并且比较packahe.json,更新依赖的版本,重新安装依赖,大功告成。

在实际的情况下,受制于项目版本控制、团队协作、迁移成本等,第一种方式,基本不可用。

解决完问题,接下来我们需要将写在config-overrides.js中的自定义配置进行迁移。

自定义配置

在进行自定义配置之前,如果项目因eslint规则,无法运行成功,可以尝试在config/webpack.config.js中找到eslint-loader并且注释掉这个配置,此时在开发模式下,eslint并不会对源码进行规则校验。

less支持

因为create-react-app脚手架并不支持less,我们需要手动添加对less的支持。首先安装less-loader,并在webpack.config.js中的module.rules下添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const lessRegex = /\.less$/;
module: {
rules: [
...
{
test: lessRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'less-loader'
),
}
]
}

注意到这里使用到了一个封装好的函数getStyleLoaders,其针对不同的css预处理器,生成适用的loader,源码非常简单:

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
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
options: shouldUseRelativeAssetPaths ? { publicPath: '../../' } : {},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
loader: require.resolve('postcss-loader'),
options: {
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
postcssNormalize(),
],
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push({
loader: require.resolve(preProcessor),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap,
},
});
}
return loaders;
};

我们知道如果使用了css预处理器,在设置loader的时候,可能需要多个loaders,例如:

  • less-loader: 把less编译成css
  • css-loader: 解决css使用importrequire引入的问题
  • style-loader: 通过style标签把css注入到DOM中。
  • 生产环境下可能还需要生成单独的css文件,css压缩等。

getStyleLoaders就是生成这些loaders的一个公共方法。

组件库按需加载

在开发过程中,如果使用到了组件库且该组件库支持按需加载(例如antd),那么我们可以根据相关的教程配置即可。

比如在当前项目中使用到了xx-ui这个组件库,该组件库支持babel-plugin-import按需加载,在安装好依赖后,我们只需要在package.jsonbabel下配置plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"babel": {
"presets": [
"react-app"
],
"plugins": [
[
"import",
{
"libraryName": "@wind/xx-ui",
"libraryDirectory": "es",
"style": true
}
],
]
}

这和传统的.babelrc中进行plugin配置无区别,但总算不用再维护额外的一个文件了。

其它

比如我们在构建时需要对代码进行拆分,我们可以在webpack.config.jsoptimization中使用splitChunks进行自定义、分离runtimeChunk等

比如我们构建时不想使用默认的static/js/[name].[contenthash:8].chunk.js作为chunks的文件名,直接修改即可。注意,在代码分割时,相应的css也会被分割,如果想修改css的配置可以直接找到MiniCssExtractPlugin进行修改即可。

最后

经过上面所有的步骤,项目已经能够完全正常运行,和eject之前一样。接下来我们可以详细阅读webpack.config.js,进行更进一步的定制,也可以删除某些项目中不需要的配置,减轻webpack.config.js的复杂性。

如果项目是老项目的话,为了项目能够在eject后不会因为eslint报错而无法正常运行,可以注释掉eslint-loader来避免每次构建时候的预检,但此时对代码风格的约束只限于编辑器本身eslint的支持,对代码质量和团队代码风格可能造成一定的影响。

更好的方式是使用一个适合这个项目的eslint规则,如eslint-config-airbnb-base,并且对这个第三方规则进行定制,直至这个规则适合这个项目。

完。

分享到 评论

老项目Webpack构建优化

最近在接手一个项目的时候,由于其开发已经一年有余,整个项目使用了create-react-app脚手架,Webpack版本停留在3.x,React版本15.x,并且在线上使用效果并不好,首页加载缓慢。对于这样一个典型的老项目,进行构建优化。

webpack 3.x之前的构建中,该项目使用CommonChunkPlugin对特定的package进行了抽取,例如react、图表库、组件库,其余使用到的package和业务代码被打包在了一起,Gzip过后差不多1MB。通过分析可以得知,分离出的第三方库和业务代码存在重复打包的情况,并且页面初始加载并不需要用到图标库,而组件库只使用到了部分组件却被完全打包了。

升级Webpack

由于使用的是create-react-app脚手架,其自带了react-scripts作为webpack的配置,本项目使用的是老版本脚手架生成,Webpack版本是3.x。分析后,在不更新脚手架的前提下,直接升级Webpack,可以使用splitChunk进行代码分隔,配合新版本的Reactlazy & Suspense,可不引入三方库的情况下实现按需加载。

升级Webpack比较容易,直接升级最新版的react-scripts即可(注意不是react-script)。

1
yarn add --exact react-scripts@2.1.1

安装完后yarn start运行项目,不出意外会有报错提示,因为某些react-scripts依赖的库版本过低,此时只要删除yarn.lockpackage-lock.jsonnode_modules,并且运行yarn install重新安装依赖即可。

安装完成后,不出意外项目可以正常运行了,此时Webpack已升级。

代码分隔

  1. 考虑到初次构建的时候仅仅分离部分第三方库导致第三方包和业务代码之间存在代码冗余,在新构建的版本中,需要对公用代码进行抽取,这样可以直接避免冗余。具体的splitChunk配置这里不再列出,抽取完成后例如lodashmoment这样的公共库被抽取到了公共包中,构建完成后生成了三个包:业务代码及未抽取的第三方包、特定的第三方包、公共代码包,构建体积明显减少。

  2. 由于业务的特殊性,首页并不包含任何图表,因此不需要加载图表库,并且图表库打包完成后大小为700KB(未Gzip),因此考虑使用基于路由的按需加载来去除首页不需要加载的资源。

  3. 进行代码分隔依赖于运行时加载,也就是import(),这是一个ES提案,目前还未标准化(babel已实现)。不同于import语法在预编译时链接代码,import()可以在运行时异步加载资源。而Webpack在解析到这种语法的时候,就会自动的进行代码分隔。

  4. 目前React懒加载的方案有很多,比如react-loadable,但由于新版的React已经支持了代码分隔,就没必要引入额外的库。React.lazy函数能够让你像处理常规组件一样处理懒加载组件。该函数接收一个函数作为参数,这个函数必须使用到import()动态加载组件,import()返回一个Promise,在resolve后返回这个React组件。Suspense是一个组件,其主要的作用是在懒加载块没有加载完成时使用占位组件进行UI 的优雅降级。

  5. 经过分析,基于路由的分隔可能最适合本项目。涉及到图表库的三个页面使用懒加载进行代码分隔,其余页面打包在一起,并且完全分离第三方库和业务代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import { lazy, Suspense } from 'react';
    import { render } from 'react-dom';
    import Loading from '@/components/loading'
    const ChartPage = lazy(() => import(/* webpackChunkName: "detail" */'@/views/chartPage'));
    const App = () => (
    <Router>
    <Suspense fallback={Loading}>
    <Switch>
    <Route path='/detail' component={ChartPage} />
    ...
    </Switch>
    </Suspense>
    </Router>
    );

直接运行yarn start,观察打包结果,发现已经按照路由进行了分隔,在总打包大小基本不变的情况下,初始加载的包只有230KB,减少了一大半。

  1. 观察打包结果,发现公共包的命名是0.hash.chunk.js,对于已经在注释中指定了webpackChunkName,这显然是不正常的,可能是react-scripts默认分隔策略的问题。为了修复这个命名问题并且进一步分隔runtime chunk,对splitChunk进行少量的配置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const SplitCodeConfig = {
    optimization: {
    splitChunks: {
    chunks: 'all',
    },
    runtimeChunk: {
    name: 'runtime'
    }
    }
    }

只需要进行简单的配置,设置splitChunkschunksall, Webpack就会使用默认的规则进行打包:抽取公共包,抽取node_modules第三方包等等,具体见Webpack官网 -> 文档 -> 代码分离

至此,项目构建已经完成,构建的包可以直接用于生产换环境。

思考

  1. 在项目中使用到的组件库包含近百个通用组件和业务组件,但是项目中只使用到了十余个,并且通过观察打包结果,可以看到未被使用到的组件也被打包进来了,这显然不合理。针对这个问题,我们可以查看对应组件库的按需加载方案,并且在Webpack进行相应的配置即可。这里由于使用的是内部开发的组件库,就不在举例。

  2. 除了使用的组件库,我们需要找到所有能够按需加载却在项目中没有使用的第三方库。例如我要使用lodashdebounce函数进行防抖,我在项目中是这样使用的:

    1
    2
    import _ from 'lodash';
    window.onscroll = _.debounce(handler);

在构建的时候,整个lodash会被打包,如果只需要打包debounce函数,在import的时候直接import debounce from 'lodash/debounce'即可。

其它

  1. 为了直观的查看构建结果,推荐使用webpack-bundle-analyzer插件,它会在构建完成后打开一个页面,该页面可以看到每个bundle的打包情况。使用也很简单:

    1
    2
    3
    4
    5
    6
    7
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    {
    ...
    plugins: [
    new BundleAnalyzerPlugin(),
    ]
    }
  2. 如果想测试Gzip后的文件大小,在webpack-dev-server的配置项中添加compress: true即可。但如果查看生产环境下构建的Gzip包大小,可以使用compression-webpack-plugin插件,该插件会在构建完成后生成对应bundleGzip包。

    1
    2
    3
    4
    5
    6
    7
    const CompressionPlugin = require('compression-webpack-plugin');
    {
    ...
    plugins: [
    new CompressionPlugin({ threshold: 8192 }), // 只有大于8KB的资源才压缩
    ]
    }

在一般的情况下开启Gzip只需要设置生产服务器即可,并不需要在构建时生成Gzip包,但如果每次请求时再Gzip无疑会加大服务器的负担,因此事先准备好gzip包,服务器不必压缩而是直接返回,这不失为一个好的选择。

开启Gzip会极大的减小传输体积,但无论是压缩还是解压都需要大量的运算,对于某些较小的资源,使用Gzip可能反而会降低性能,因此建议只对较大的资源进行Gzip压缩,而较小的资源直接传输。

完。

分享到 评论

Webpack 4.x 简易使用指南

本篇来自于之前在团队内部分享webpack时的笔记,内容比较浅显,主要讲webpack基本配置,同时webpack运行原理,自定义loaderplugin也有涉及。

Webpack4.x使用指南

零配置使用Webpack

从Webpack4开始支持零配置,在不进行任何配置的形式下,直接运行webpackwebpacksrc/index.js作为入口,最终结果输出到/dist

在默认情况下,webpack只会对只会从入口开始遍历所有依赖文件,由于没有配置loaders,因此无法处理jsx/css/图片等。

在默认情况下运行webpack,打包完成后控制台会提示在配置打包的模式。在cli中通过设置--mode=production|development可以指定默认,在开发模式|产品模式下,webpack会默认启用不同插件对打包进行优化。

webpack基本配置

  1. 入口(entry)
    入口的配置形式有多种,例如:

    1
    2
    3
    4
    5
    6
    entry: './src/index.js', // #1
    entry: {
    home: './home.js',
    about: './about.js',
    }, // #2
    entry: ['./home.js', './about.js'] // #3

    第一种方式配置单一入口,所有文件将会被打包到同一个文件中,例如默认将会打包到main.js中;第二种方式是多页面(MPA)的配置方式,配置多个入口,最终将会生成多个bundle,配合html-webpack-plugin可以将对应的bundle注入到对应的html中;第三种配置的形式指定了多个入口,但是会将最终结果打包到一个文件中。

  2. 输出(output)
    输出指定了webpack将结果输出到哪里,如何进行输出,甚至是使用何种方式构建等,配置比较复杂。常用配置举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    output: {
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, 'dist'),
    }, // #1
    output: {
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, 'dist'),
    library: 'xxLibrary',
    libraryTarget: 'amd',
    }, // #2

    第一种配置方式是最基本的输出配置,它配置了输入文件的存放的目录以及文件命名,filename支持模板语法,[name].[hash].js表示使用模块名称(入口配置)以及模块标识符来命名文件。
    第二种配置增加了’library’, ‘libraryTarget’两个属性,而library值的作用取决于libraryTarget的值。例2中配置libraryTarget: 'amd'表示使用amd的方式打包,而library的值将作为模块名。

    1
    2
    3
    define('xxLibrary', [], function() {
    return _entry_return_;
    })

    而当libraryTarget: 'commonjs2'时,表示该模块将使用commonjs的方式打包,用于node环境,而library将不起作用。此外libraryTarget的值还可以是var(默认)this等。

  3. 加载器(loader)
    在webpack中module决定了如何处理项目中不同类型的模块,而加载器(loader)则是具体的处理逻辑。

    noParse:指定不需要进行构建的文件。例如,指定项目中不需要对jquery进行构建,可以进行如下配置:

    1
    2
    3
    noParse: function(content) {
    return /jquery/.test(content);
    },

    规则(rules)的配置同样比较复杂,常用的配置如下:

    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
    rules: [
    {
    test: /\.(js|jsx)$/,
    exclude: /(node_modules|bower_components)/,
    include: /src/,
    use: 'babel-loader'
    }, // #1
    {
    test: /\.(png|jpeg|gif)$/,
    oneOf: [
    {
    resourceQuery: /inline/,
    use: 'url-loader',
    },
    {
    resourceQuery: /external/,
    use: 'file-loader',
    }
    ]
    }, // #2
    {
    test: /\.css$/,
    use: ExtractTextPlugin.extract({
    fallback: 'style-loader',
    use: [
    {
    loader: 'css-loader',
    options: {
    modules: true
    },
    },
    ],
    }),
    },
    ], // #3

    上面列举了常用的配置loader的三种情况,第一种处理js|jsx文件时,使用excludeinclude指定构建目录和不需要构建的目录,对于明确不需要构建的模块进行指定可以加快构建速度。第二种情况则是根据具体情况选择对应的loader进行处理。例如当我们使用import emoj from './emoj.png?inline'时,则会使用url-loader对该png文件进行处理,对于external类型的图片,则使用file-loader处理。第三种情况则是使用多个loader并且可以传递options,例如处理css文件,这里使用了style-loadercss-loader两个加载器,并且配置了css-loader的optionsmodules: true。这里需要关注loader执行顺序,loader执行的顺序是先配置后执行,例如这里先使用css-loader解析css,再使用style-loader配置将生成好的css文件通过<style>标签注入到DOM中。options配置了modules: true,则开启css-modules,写法如下:

    1
    2
    3
    4
    import style from 'index.css';
    let IButton = ({children}) => (
    <span className={style.btn}>{children}</span>
    );
  4. 插件(plugin)
    插件(plugin)用于以各种方式自定义webpack构建过程。webpack附带了非常多的插件,可以直接使用webpack.pluginname使用,也存在非常多的第三方插件。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    plugins: [
    new CleanWebpackPlugin(['dist']), // #1
    new HtmlWebpackPlugin({
    title: 'index',
    chunks: ['index'],
    }), // #2
    new BundleAnalyzerPlugin({
    analyzerMode: 'server',
    analyzerHost: '127.0.0.1',
    analyzerPort: 8889,
    }), // #3
    ],

上例使用了三个插件,第一个插件CleanWebpackPlugin作用是在构建输出前清空文件夹;第二个插件用于生成入口html,例如该htmltitle,指定其需要注入的bundle(MPA)。第三个插件则是用于分析构建结果的插件,它会在本地启动一个开发服务器,并且通过canvas来绘制构建可视化结果,方便进行构建分析和优化。

  1. 开发服务器(devServer)
    webpack-dev-server方便我们进行快速开发应用,它会对代码进行监控,一旦发生更改,它会立即构建,并通过补丁的方式应用更改,使得应用能够快速应用更改。并且其提供了一个基于Express开发的简易服务器,所有资源文件都存在内存中,访问速度极快,并且通过配置可以支持热替换。介绍一下常见的webpack-dev-server配置。
    1
    2
    3
    4
    5
    6
    7
    devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    inline: true,
    historyApiFallback: true,
    allowedHosts: ['host1.com', 'host2.com'],
    }

contentBase主要用于指定静态文件的存放路径,例如所需要图片等,如不指定则无法找到对应文件;

compress表示是否开启Gzip压缩;

inline表示是否启用内联模式,内联模式:实时重载的脚本会被插入到bundle中,构建消息将会出现在控制台。此外还有iframe模式。

historyApiFallback主要针对的是访问不存在的页面时的活动,historyApiFallback: true时访问不存在的页面会直接跳转到index.html,也可以传入一个对象更精确的进行控制跳转。

allowedHosts用于指定允许该devServer的主机。

hot是否开启热替换,如果为true的话,则会使用webpack.HotModuleReplacementPlugin插件。通过CLI的方式传递。

1
webpack-dev-server --hot

Webpack 运行流程

概括上来说:
初始化配置参数 -> 绑定事件钩子回调 -> 确定Entry开始遍历 -> 使用loader编译文件 -> 输出

webpack就像一条生产线,经过一系列处理流程后才能将源文件转换成输出结果。

每个流程的处理职责是单一的,流程之间存在依赖关系,只有当前处理完成后才能交由下一个流程处理。

插件像是插入到生产线上的一个功能,在特定的时机对资源进行处理。

webpack 通过 Tapable来组织这一切。

Webpack在运行过程中会广播事件,插件只监听它关心的事件。

—《深入浅出webpack》

Tapable的核心代码可以简化成。

1
2
3
4
5
6
7
8
9
10
11
12
13
class SyncHook {
constructor() {
this.hooks = [];
}
// 订阅事件
tap(name, func) {
this.hook.push(func);
}
// 发布
call() {
this.hooks.forEach(hook => hook(...arguments));
}
}

Webpack具体的流程如图所示。
流程

  1. 初始化构建参数(依据webpack.config.js),插件实例化,生成Compiler供插件使用,挂载自定义钩子。

  2. 依据入口递归遍历文件,并且使用对应的loader进行编译。

  3. 将编译好的文件解析成AST(抽象语法树),分析依赖逐个拉取济源。

  4. 编译完成,输出。

编写自定义loader

在webpack中,真正起编译作用的就是各种各样的loader。loader其实是一个function,其传入匹配到的文件内容(String),然后对这些内容做处理即可。

一个最简单的loader可以使下面这样:

1
2
3
module.exports = function(content) {
return "{};" + content;
}

config中进行配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
module: {
rules: [
{
test: /\.js$/,
loader: path.resolve(__dirname, './loaders/index.js'),
options: {
param1: 1,
param2: 2,
}
}
]
}

这样在编译JS的时候就会在每个JS文件前加上{};


获取自定义配置

可以使用loader-utils模块来拿到自定义配置。

1
2
3
4
5
6
7
const loaderUtils = require('loader-utils');
module.exports = function(content) {
let options = loaderUtils.getOptions(this);
console.log(options.param1); // 1
console.log(options.param2); // 2
return "{};" + content;
}


数据导出

loader可以通过return来返回处理后的结果;当然,更好的方式是使用this.callback的形式。因为它更加灵活,除了content以外,还可以传递其它参数。

this.callback(error, content, sourceMap, ast)可以传入四个参数:

  • error loader向外抛出一个错误
  • content 经过loader编译后的内容
  • sourceMap
  • ast 本次编译生成的AST,之后执行的loader可以直接使用,而不需要再次生成
1
2
3
4
5
6
7
const loaderUtils = require('loader-utils');
module.exports = function(content) {
let options = loaderUtils.getOptions(this);
console.log(options.param1); // 1
console.log(options.param2); // 2
this.callback(null, "{};" + content);
}

异步loader
对于异步loader,可以使用this.async来获取
callback函数。

1
2
3
4
5
6
7
8
9
10
11
const loaderUtils = require('loader-utils');
module.exports = function(content) {
let options = loaderUtils.getOptions(this);
let callback = this.async();
this.cacheable(false); // 是否缓存结果
console.log(options.param1); // 1
console.log(options.param2); // 2
setTimeout(function() {
callback(null, "{};" + content);
}, 1000);
}

pitch钩子
可以在loader文件中exports一个名为pitch的函数,它会先于所有的loaders执行。可以在这个过程中传参,而当前rule的所有loaders都可以拿到这个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const loaderUtils = require('loader-utils');
module.exports = function(content) {
let options = loaderUtils.getOptions(this);
let callback = this.async();
this.cacheable(false); // 是否缓存结果
console.log(options.param1); // 1
console.log(options.param2); // 2
console.log(this.data.hello); // hello
setTimeout(function() {
callback(null, "{};" + content);
}, 1000);
}
module.exports.pitch = function(remaining, preceding, data) {
data.hello = 'hello';
}

Plugin 初探

Plugin起始是一个简单的class,有一个必须要实现的apply方法。

1
2
3
4
5
6
7
8
9
class LPlugin {
constructor(options) {
this.options = options;
console.log('options', options);
}
apply(compiler) {
console.log('run this plugin');
}
}

使用

1
2
3
4
5
6
7
8
9
10
const LPlugin = require('./plugins/LPlugin');
module.exports = {
...,
plugins: [
new LPlugin({
param1: 1,
param2: 2,
})
]
}

plugin在初始化参数就进行实例化(事件流开始时),因此类似于CleanWebpackPlugin在构建之前进行文件操作删除掉某些目录也是很好实现的。


Tapable & Hook Tapable是Webpack构建过程中的核心所在,其暴露了tap tapAsync tapPromoise等方法,可以使用这些方法,来注入一些逻辑,这些逻辑将会在构建过程的不同时机触发。

例如:

1
2
3
compiler.hooks.compile.tap('LPlugin', params => {
console.log('触发了钩子函数');
})

该钩子函数将会在compile阶段触发。

*** 自定义钩子函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { SyncHook } = require('tapable');
class LPlugin {
constructor(options) {
this.options = options;
console.log('options', options);
}
apply(compiler) {
compiler.hooks.LPlugin = new SyncHook(['data']);
compiler.hooks.enviroment.tap('MyPlugin', function() {
compiler.hooks.LPlugin.call('hello');
})
console.log('run this plugin');
}
}

1
2
3
4
5
6
7
class LLPlugin {
apply(compiler) {
compiler.hooks.LPlugin.tap('LLPlugin', function(data) {
console.log('data', data); // data hello
});
}
}
分享到 评论

使用Webpack DllPlugin

DllPlugin 简易使用指南

  1. 创建webpack.dll.config.js 用于对特定的模块打包成dll
  2. webpack --config webpack.dll.config.js 生成dll以及其描述文件
  3. webpack.common.config.js中使用DllReferencePlugin引入打包好的dll文件。
  4. 打包。此时遇到相应的模块时直接引入而不会重新打包。

创建webpack.dll.config.js

这里以分别打包momentlodash为例

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
// config.js
module.exports = {
entry: {
lodash: ['lodash'],
moment: ['moment'],
}
}
// webpack.dll.config.js
const webpack = require('webpack');
const path = require('path');
const config = require('./config');
module.exports = {
entry: config.entry,
output: {
path: path.resolve(__dirname, '/static/dll'),
filename: '[name].dll.js',
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, '/static/dll', '[name]-manifest.json'),
name: '[name]_library',
context: __dirname,
})
]
}

解释:

  1. 推荐把入口配置信息写在单独文件中,易于维护。
  2. DllPlugin类接受一个配置对象,该对象有三个属性:context(绝对路径), manifest文件中请求上下文;name,暴露的dll函数名;path:manifest文件存放的位置(绝对路径)。

使用DllReferencePlugin

1
2
3
4
5
6
7
8
9
10
11
12
// webpack.common.config.js
const config = require('./config');

module.exports = {
... // 省略其它配置
plugins: [
...Object.keys(dllConfig.entry).map((name) => new new webpack.DllReferencePlugin({
context: __dirname,
manifest: require(`./static/dll/${name}-manifest.json`),
}))
]
}

DllReferencePlugin 接受一个对象用于初始化

  1. context: manifest的上下文(绝对路径),需和DllPlugin中的context一致
  2. manifest: manifest文件,使用require引入或指定绝对路径

可选参数:

  1. content:模块id的映射,默认为 manifest.content
  2. name: dll文件的名称,默认为 manifest.name
  3. scope: dll 内容前缀
  4. sourceType: dll如何暴露的?amd commonjs2 …

scope: ‘abc’, 则该dll中的xyz文件可以通过require(‘abc/xyz’)来引用
例如在一个dll中打包了lodashaxios两个库,并且指定了{scope: 'lib'},则在需要使用axios的时候使用require('lib/axios')即可。

测试1:把echartswinduireact及其周边分别抽取成dll文件。

抽取的dll文件大小分别为:753KB 1603KB,385KB。

构建方式 平均构建时间 包大小
普通 97s 4.1MB
使用Dll 71s 2.6MB
dll文件 24s 2.7MB

经过对比发现,由于无法使用按需加载,所以整个windui打包的大小差不多为1.6MB,而普通的打包方式windui的大小仅为365KB。

在使用windui链接库后生成的打包,发现vendors模块中仍然含有windui,大小为330KB,仅windui中的node_modules文件夹下的rc-triggerrc-dropdown被重用,因此这里可能重复打包了。

windui中依赖的rc-*部分模块仍然被打包。

方式二 仅对react相关模块和echarts进行打包

抽取的两个dll文件的大小为393KB 769KB

构建方式 平均构建时间 包大小
普通 97s 4.1MB
使用DLL 77s 3.08MB
dll 11s 1.16MB

windui的大小基本不变,而windui依赖的rc-*系列组件基本没被打包。

vendors模块从0.61MB1.67MB

使用html-webpack-include-assets-plugin 把dll注入到index.html

1
2
3
4
5
const HtmlWebpackIncludeAssetsPlugin = require('html-webpack-include-assets-plugin');
const dllIntoHtml = new HtmlWebpackIncludeAssetsPlugin({
assets: ['./static/dll/echarts.dll.js', './static/dll/react.dll.js'], // 需要注入的dll文件路径
append: false, // 是否尾注入?push : unshift
});

因为先构建dll,再进行项目构建,在项目构建过程中会删除整个build目录,所以在构建完后再把dll文件夹拷贝进build/static/

总结

  1. windui进行dll打包后会因为无法按需加载而导致总大小偏高(且可能会产生冗余);而不进行的话,包的总大小基本不变。

  2. 初始时需要加载的chunk从3个变为6个/5个

升级到webpack4.x

升级到webpak4.x后无论是dll还是最终生成块,都有小幅度的下降。与此同时,构建总时间(dll构建时间+项目build时间)略微减小。

关于使用已存在的模块直接作为dll文件引入的可能性

  1. 目前无相关方面的实践。
  2. DllPlugin抽取特定的模块构建dll文件后会生成一个manifest文件。该存储了各个模块的和公共模块的对应关系。
    该文件会对已经打包成dll的模块中的文件进行描述,会给每个文件指定id,并且该json文件中的name属性对应dll的library
  3. 在进行项目构建的时候,如需要打包某个模块,会在manifest文件中查找,如果该模块已经存在于dll中,依据manifest中的信息进行链接即可,不必重新打包。

构建的dll块和使用splitChunk或者直接使用babel打包出来的块不一致。其依赖构建dll时指定的library等,而manifest文件也是和该块一一对应的。所以从理论上,使用其它方式提供dll文件由于构建方式和无法提供manifest文件,在构建过程中并不能被重用。

PS: 本片文章来自于我在团队内部分享的笔记,任何关于webpack-dll-plugin的理解和使用方式请在该插件的Webpack介绍页查看

分享到 评论

我的这一年

好一个寒冷的假期!

最后一日

早上七点,闹钟开始吵个不停,我习惯性按下锁屏键,并快速蜷缩到被窝中,十分钟后,我再次重复这两个动作,直到不得不起。可是,虽然是周一,可毕竟是假期呢。这次我直接打开了闹钟,取消了激活状态,这下,可以好好睡一觉了。

再次醒来的时候,已经快十一点了,湖人的比赛已经开打,也没了吃早饭的必要。我摸出手机,打开“掘金”,果然,技术社区唯一的小说也停更了;今天是周一啊,我似乎忘记了点什么啊。没错!“龙五”更了,看饿了看进度条99.1%,果然,老贼可不会因为今天是18年最后一天,就爆更的。剧情延续上一章,雪救下了阿巴斯,拯救了整个船上的人,而现在船员都知道雪是一个怪物了,要处决她。现在该由阿巴斯救雪了,阿巴斯夹带着言灵“因陀罗”登场,本章完。按照老贼的尿性,在阿巴斯救下雪这个过程中再水个三五章不无可能。哎,什么时候到个头呢!

好不容易起了床,洗漱完毕,打开电脑,开始了一天的“工作”。打开NBA 2K Online2,先晚上两把,发现我已经11连胜了;打开某鱼直播平台,看狗贼叔叔打两把“沙包战”;同性交友社区GitHub当然也不可少,看看前端娱乐圈,今天是否又有猛料。一切完毕,吃饭的点到了,刚刚好。

要是在上班,直接电梯直下十层,食堂的饭虽然不好吃又凉,但架不住便宜并且还少了奔波,所以单从填饱肚子的角度上来说,还行。而周末或是假期就不一样了,自己做饭是不可能的,那么除了点外卖就只好出去吃了,还是在这寒风刺骨的冬天。

穿好厚重的羽绒服,匆匆下了楼,今天我并不想图近就在附近随便吃点,而是要走过好几个路口去公司旁边的小店吃。虽然说实话,作为一个重庆人,从小色香味俱全的菜吃多了,反而觉得这边的什么都不好吃,并且还贵。可是呢,生活还得继续,所以在吃这方面,我能选的只有分量大/价格便宜,如果能够稍微好吃点,那可就是极好的了。

去公司的有两条街最近一直在施工,这不,路过红绿灯的时候,送外卖的小哥把车开进刚铺好的水泥路上,直接连人带车陷进去了。这么冷的天,弄得双腿都是未干的水泥,车也暂时开不了了,不知又会耽搁几个外卖,损失多少收入呢。前天晚上给母亲打电话,都很晚了父亲还在工地上没下班,并且他们基本上是没有休息日的,连节假日也不例外。在我心中,劳动人民都是伟大的,值得所有人的尊重。

刚开始写这篇文章的时候,我开着窗户,吹着寒风;可是不一会儿,就冻得受不了,只好打开空调,顿时暖风阵阵,僵住的双手得以解冻,恢复了往日的活力,然后电费飙升,令人心疼。

预计我会在写完文章过后玩两把吃鸡,到了饭点,我会叫上一顿丰盛的外卖,整个晚上,会找一些还没看过且是科幻/战争题材的电影看看,往后还有让人拍案叫绝的2K剁手环节。我的2018最后一天,会这样度过。

毕业设计

我已经不记得2018年的第一天,我具体干了什么。按时间来算,应该在准备期末答辩,然后开始享受学生时代的最后一个寒假?

我记得春节刚过,我就来到了学校,那个时候软件园水池里还结着厚厚的冰,健身房的门还没开。我选的毕业设计是一款Web应用的开发,并且要求的是使用Vue开发,那个时候我对Vue的了解仅限于听过它的名字,仔细看完一遍官方文档,我就和导师的研究生们开始开发这个应用了。其实对于这个合作的项目,我多有吐槽,这本是一个为收集用户行为数据并进行进一步分析的应用,从应用本身的规划来说没什么问题,但问题就出在开发者本身上面,效率真的很低。每次9点到实验室结果研究生哥哥姐姐们还没到;约好时间进行接口对接,结果竟然在玩手游;一周下来,一个页面都没做好。鉴于此,我决定给我的导师摊牌,我可以协助完成整个应用的开发,但是对于我的毕业设计,我想自己开发。后来,我花了差不多一周时间完成了管理员端的开发,协助完成了接口开发,解决他们遇到的几乎所有问题,然后,脱离了这个合作项目。

其实,不应该只有吐槽。负责接口开发的学长才研一,跨专业考的软工,并且我们的后端使用的是Spring Boot,对于一个Java都怎么熟悉的人,能够在短时间之内完成任务,并且每次我们交流的时候他都在工作,就冲这股认真劲,给他点赞。另外一个就是负责开发应用的学姐了,在整个应用中,我和她接触最多。她对前端也不怎么了解,一个月下来也能做得有模有样,并且干起活来特别认真,一个特别热心肠的人。认真努力的人,值得我们尊重,并抱有敬意!

我的毕业设计脱胎于这个合作项目,是一款基于位置感知的任务众包平台。至于位置感知,就是对于绝大多数本地线下任务而言,推送给用于是基于位置的。此外,还构建了基于统一交易凭证的支付平台,内容审核,用户论坛等。从前端到接口,完全由我一个人开发。当然,直到最后毕业答辩,拿到优秀后,我仍然觉得这个应用还只是一个demo,只完成了所有设想的功能,页面也仅符合直男审美。

这是我的一次尝试,我设想着把想法付诸行动,并且在二十多个下午,我完全进入了编程状态,带上耳机,排除一切干扰,尽量把工作做好。而这样的体验,也只有15年在西电的一年时光。除此之外,对于在项目中用到的技术/框架,我加深了理解,我开始了解到单页应用服务端渲染,对web前端,以前可能只是开了一户小窗,而现在简直是开了一扇门。

毕业季

在毕业前几天,我来到上海,租好房,开始迎接新的生活,在此之前,似乎还重要的事情等待去做。

其实放在以前这件事是我想都不敢想的,这当然不是向喜欢的女孩子表白,更谈不上惊天动地。说到底,是我一个人的执念罢了。在22岁这个年纪,在开始一段全新生活之前,想要解开的心结。

而来到上海入职的前几天,我买了到福州的火车票,然后在那里呆了一天,见到了熟悉的陌生人和她所见的世界。回上海的那一天晚上,刚下过大雨,我从地铁站回公寓,听见小区里的蛙鸣,在这个寂静的夜里,我把什么都抛到了脑后,一切都在那一刻结束了。

不枉我耿耿于怀那么多年。

工作

我准备好了工作,可是真到了那一天,我发现这不是我想要的。

其实工作说得很多了,在很多周记里,我都提到这周完成什么样的工作。半年下来我的态度也在慢慢发生改变,我不得不去做自己不那么感兴趣的事,我对重复性劳动也没那么排斥。我能够利用工作中空余的时间片段,学到更多的知识。

其实自己对工作还是多有不满的,这种不满最初来自于对工作预期的落差,后又因为莫名其妙的转岗雪上加霜,现在因为重复性的劳动。除此之外,我接触到的同事,都非常nice;还有我们的小伙伴们,真的幸运能够和你们认识。

说说最近吧,部门新来了一名实习生,说是做后端的,但是由于我们项目缺人,所以就过来搞前端。大概工位离我很近的原因,我成了他指定导师。说是导师,其实也就是他遇到工作中的问题,我负责给他解答而已。小明才来了几天,一天趁晚饭时间,和他聊了不少。从在校,学习到面试基本都问了下。这位小明同学给我的印象就是,他仅仅是一张白纸而已,说是做后端,但一问到也说不出个所以然来;前端?抱歉也许以前做课程设计的时候可能写过JSP。所以说,我打心底里根本没想让他在短时间加入到这个项目中。这种他最主要的任务就是学习,从JavaScript语法开始;可刚学一天,他就给我说,学习没有目的性;刚过一周,他不但把JavasScript看得差不多了,React也加入了技能树。可是当我问的时候,我说React的生命周期有没有理解,propsstate是怎么回事,命令式声明式区分开了吗,他却说不出个一二。然后我又花很多时间给他讲。我想到我自己,从大二开始接触前端,很久也对JS不怎么理解,React差不多接触了一年多,写了好多Demo才能达到熟练使用用的程度,是我太笨了,还是他们他浮躁呢?所以,小明在下个迭代会加入我们,并且我给他安排了差不多我1/3的任务量,希望他能够快速成长吧。

关于我正在做的这个项目,经历了刀耕火种的时代,在开发过程中大家逐步填坑,并且为了解决开发过程中的痛点,开发了许多轮子;直到现在,我们打开发效率很高了。除了免不了成为配置工程师或者说流水线工人,我想说,我喜欢这个团队。希望项目早日完成/找到接盘侠,脱离苦海。

生活

生活也在周记中提到很多了,每天除了工作,下班了我基本上不会选择继续学习,可能会看看直播玩玩游戏。周末,多睡睡觉,打打篮球,看看电影放松放松。可不是不知为何,一到周一上班特别累。

本来想着来工作了继续健身,所以办了健身卡,每周4次的量,可是奈何健身房跑路这种奇葩的事情都能让我碰见,所以健身就搁置了。每周会花一些时间利用弹力绳保持下身材,所以到现在还没变成肥宅,可是看见日渐隆起的小腹,心中的担忧又来了~

未来

当初我憧憬魔都才选择来到这里工作,可是半年过去了,我丝毫没有归属感。除了同事和舍友,我基本不认识其它什么人;土著们操着一口你怎么也听不懂的方言;这里的物价水平也高到离谱,房租花费3000+,随便吃点什么也都得花费两倍于家乡甚至更高。最奇葩的是,隔壁的老太遇到过我两次,每次都问很久,结果他担心的是外来租客可能导致不安全的问题。虽然老太并没有恶意,但是我却心底拔凉拔凉的,我们是怀着梦想来到魔都,给魔都带来活力,我们买不起房,上下班骑摩拜,我们蜗居在10平米的小屋里却交着巨额房租,而现在他们不欢迎我们,担心我们打破他们的生活宁静,可那又有什么办法呢,就算是这样,生活还得继续不是吗?

所以我每次都认真回答老太的问题,并且打消老太这方面的顾虑。我喜欢魔都的生活节奏,但我看到每个月到手的工资和交完房租和生活必须花费后存下的钱,我想,我的未来不会在这里。至于下一站在哪里,何时到站,还没有计划,但可以预见的是,不会太久。

无论是到哪里,都要不虚此行,所以珍惜每个平常的一天,让自己能更进一步,变得更加优秀。

尾巴

还有不到7个小时就2019年了,在2018年我结束了学生时代,并且在22岁的年龄开始独挡一面。我以前说过,我从小到大几乎所有的决定都是我自己做的,而家人们总是尊重我的决定并且无条件的支持我。我想,什么时候他们能给我一个决定呢?好吧,就让它在2019年吧,说到做到!

愿好,明年见!

分享到 评论

每周一记?

最近总想着升级,比如玩2K阵容想升级,为了追剧想升级成为XX视频会员,为了得到更好的电竞体验,想升级我这台还不怎么过时的PC。当然,其实写周记也要升级了,升为月记可好?

废话少说,不然电竞时间不够用啦。

学习

项目在迭代后期照例改改bug划划水,所以空出来大把大把的时间,去掉刷微博看NBA,貌似还剩点时间片,所以还是装模做样,假装学习。

  1. DOM Element 位置相关的属性

这可是学了又忘,用到查资料的东西,总是会把这么些属性搞混。

client-*: 不用说,这个还是记得住的。

scroll-*:如果是描述形状的话(width&height),那就是元素(及padding)和溢出区的宽/高度,如果是位置,则表示向下/右滚动的距离。

offset-*: 基于元素的offsetParent来进行形状和位置的计算。而offsetParent则表示距离元素最靠近的上层元素,且其position不为static。如果是表示形状,则表示其水平/竖直方向上的高度(包含padding和border); 如果表示位置,则表示距offsetParent`的距离。

再认识一下getBoundingClientRect()用于获取盒模型的各种属性。
其中x,y表示其相对视口的距离,widthheight等于(content+padding+border)。其余四个表示位置的属性分别表示据左上角/右下角的距离。

  1. Node.js 核心模块回顾

net 用于创建TCP Server和Client.

1
2
3
4
5
6
7
8
9
10
11
net.createServer(function(socket){
socket.on('data', (chunk) => {
console.log('data from client: ', chunk.toString())
})
socket.write('hello');
})
const client = net.createConnection(options)
client.on('data', (chunk) => {
console.log('data from server: ', chunk.toString())
})
client.send('hello server')

path 工具模块

1
2
3
4
5
6
7
path.basename() 文件名
path.dirname() 目录名
path.extname 扩展名
path.format(ooption) 将一个对象格式化为一个路径
path.parse(path) // 将一个路径解析成为一个路径对象
path.join() // 路径拼接
path.resolve() // 将路径片段拼处理成绝对路径
  1. React 新特性学习

现在React存在的问题:包装地狱 庞大的组件 class组件

hook:在function组件中使用到class组件中的很多特性

useState为组件添加状态
useContent 组件上下文
useEffect 用于处理副作用

自定义hook

  1. Webpack 4.x 之 code split

webpack默认的分割规则:

  • 新的代码块被共享,或是这些模块来自于node_modules文件夹
  • 新的块压缩前大于30KB
  • 按需加载的块,并行请求数小于等于5
  • 初始加载的块,并行请求数小于等于3

    默认情况下只会影响到按需加载模块,否则所有模块都会被打包到一起。

    使用ES提案中的动态加载方案,import(),则会进行按需加载并单独打包。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    optimization: {
    splitChunk: {
    chunks: 'all|initial|async'
    minSize: number,
    minChunks: number,
    maxAsyncRequests: number,
    maxInitialRequests: number,
    name: string,
    cacheGroup: {
    test: regexp,
    priority: number,
    reUseExitintChunk: boolean,
    }
    }
    }

指定chunks为all|initial会把node_modules中的模块分配到vendors缓存组,而重复至少两次的代码将会被打包到default缓存组。

initialall的区别是:当按需加载时,initial会分开打包,而all会统一打包。

提取特性的第三方库,可以直接新建一个缓存。

其它

最近在看的番《史莱姆》和《猪头少年》。

最近在看的剧《人不彪悍枉少年》。

值得一看的美剧《地球百子》系列。

特别推荐科幻美剧《无垠的太空》系列。

绝对不推荐的游戏《绝地求生》,打到自闭。

尾巴

一个多月没打篮球了。还记得刚来上海的时候,几乎每周都要去一次甚至闲的时候还会去两次,那个时候打球,特别容易迷失。如果被分到了很强的队伍,很容易隐身,超没存在感;如果分到了较弱的队,需要我进攻的时候,总是犹豫不绝;再加上时准不准的投射水平,突破终结能力实在一般,几乎每次打球都不会体验很好。

后来,既然做不好进攻,那就好好防守。毕竟身体还算强壮,下盘还很稳,又不惧对抗,最近几次打篮球在防守端还是不错的,对位同等身高的人,一点都不虚。防守好了,也带动了进攻,更加坚决的出手,往往可以收到意想不到的效果。

我差不多从上初中就开始打球,天赋平平,基本功又不够扎实,所以这么多年来基本上没什么长进。以前我十分乐意分享球,想成为一名组织者,可慢慢打着变成了神经刀。最近小伙伴叫我罗伯森,因为我的表现的确像。而我也乐意接受这样的角色,专注于自己擅长的事,成为一名好队友,一名好的角色球员

我曾经打篮球打到自闭,一度放弃篮球。可是,我还是喜欢玩篮球游戏,也无时不刻关注NBA,我也有我喜欢的球星(偶像)。说到底,我还是喜欢篮球啊。我的生活可少不了篮球。

生活也是这样,作为一个平庸的人,也要努力找到自己的擅长的点。

分享到 评论

每周一记(十一月)

先说重点

差不多一个小时以前, IG 3-0零封 FNC拿到了 LPL赛区的第一个世界冠军。我目睹了这一切,也曾今是一个英雄联盟玩家。还记得第一次弟弟带着我玩这款游戏,我抽到了一个8折优惠,花了36买了我的第一个英雄–金克斯,后来,买了第一款新年限定皮肤–羊年限定金克斯;也买过源计划--艾希。记得大学的第一个暑假高中同学小聚,网吧开黑玩一整夜也不会觉得累。记得最孤独无助的2015年,一个人在宿舍打出排位36连胜。记得几个月以前,快要离开大学之前,和同学们开黑的那段时光。老实说,当我忘记了出门要买装备,总是忘记交闪现,也经常线上漏刀的时候,英雄联盟早已不属于我了。可我总是会在忙得不可开交的一天后,打开直播看上两局;也全程关注了S8世界总决赛;也会打开网易云音乐,单曲循环RISE。到现在,我明白了,我还是喜欢英雄联盟这款游戏,即便我不怎么会亲自去玩了。

我一直喜欢玩游戏,从最开始的穿越火线,再到陪伴了整个高三的NBA 2KOL,到英雄联盟,在到如今的绝地求生。因为只有在玩游戏的时候,才是那个没有任何防备的自我。

工作&学习

为什么要把工作和学习放到一起来说呢?因为这一周真的很闲,临近项目结项,我最主要的工作,除了处理BUG,大把大把的时间就拿来学习了。

当然还是先将工作。我们的项目遇到了一个BUG,我们有一个嵌套层级很深的菜单,需要实现几个需求:

  1. 当用户切换主菜单的时候自动滚动到顶部。
  2. 当弹出板块树或者是进行滚动的时候,始终保持用户选择的菜单项在可视区域内。
  3. 当用户刷新整个页面的时候,能够准确还原到上一次用户选择的菜单项中。

我们最初并没有将Menu写成一个单独的组件,而是把它放在了DataReport中,并且只是用了scrollIntoViewIfNeeded来让选中的菜单项滚动到可视区域,因为在切换菜单是默认会选中顶部第一项的,所以不需要加任何代码就可以实现滚动到顶部这个功能。

理论上是这样的,并且我们也是这样做的。可我们发现了,如果我们切换菜单的时候,如果默认选中的不是报表,那么就没办法滚动到顶部。而且一旦刷新页面,用户当前的状态也就丢失了。

我第一想法是把Menu作为一个组件独立出来,这个我们可以更好的在组件内部实现逻辑。当切换菜单的时候,我使用了scrollTop=0这种方式来滚动到顶部。并且为了能够在刷新后记住用户状态,我需要反向遍历菜单树,并与当前路由进行对比,确认用户当前打开的菜单项位于的层级,从而控制其祖先菜单的打开和关闭。至此,问题解决了。

可是,在和boss沟通的时候,他第一时间判断我这样做是不正确的,并且现场撸了一个版本出来,且不讨论是否能用,但是太多的if else条件判断,反而让DataReport变得无比臃肿,再加上到下班时间并且我们在他身后看他写代码很久了,我一时口嗨就说了句写得乱七八糟的,这惹怒了我的boss,我也当场为我的不理智做出了道歉。
通过这件事可以看出来我的情商还要加强。

虽然最后还是采用了我的方案。

再讲讲学习。

在掘金上看了很多文章,都是零散的,比如说事件循环Shadow-Dom&Custom-Element,还是CSS mask&clip或是markdown2html,通过这些零散的知识点学习,我觉得我对前端的理解又有了更多的理解。在大方向上的学习上,主要是TypeScriptEcharts自定义系列,以及读源码。

特别是读源码,真的让我收获颇多。最近在看Wind Design的很多组件的源码,大体上是老版本的Ant Design的源码。很早就接触并使用到了Antd组件库,比如经常使用的Form组件却不知道是如何实现的。

我之前也按照自己的想法,封装了一个组件。可当读完源码后发现,真的是自己水平太低,很多问题都没有考虑到。并且作为源码,自己的代码也不够规范。

当然,从下周开始,我不会像现在一样闲了,但源码我还是会一直读下去。如果有时间的话,我会考虑记录记录写成博客什么的?

尾巴

上周末我把和上司闹不愉快的事情告诉了家人,而一到周一下班,妈妈就打来电话,问我的情况,生怕我和上司的关系存在问题什么的。说实话,我很感动,我给她说啊,都是技术上的事,不会带入平时工作的,并且我在意识到自己的错误的时候,也立马进行了道歉,况且我的上司也是一个很nice的人,这样他们终于不用担心了。其实就是这样,最关心我们的,非家人莫属。所以啊,都那么大的人了,做事情一定要考虑更多,让他们和关心你的人,少为你操心。

IG都夺冠了,那我也要戒掉自己的某个陋/懒习了,所以在最后立一个只有一个人知道的FLAG

分享到 评论