Tree Shaking In Practice

引言

随着产品的迭代升级和人员变更,我们的代码往往会逐渐变得不可控,有一点表现的尤为明显,即之前的代码逻辑模块被废弃不用,但是该模块又被引入了进来,对应的模块在编译理论中被称之为 Dead Code。我们可以采用相应的方法把这些 Dead Code 删除掉, 这种解决问题的方法被称为 DCE(Dead Code Elimination)。而 DCE 在前端领域中的实现就是常常被提到的Tree Shaking

Tree Shaking 的作用

Tree Shaking 的相关概念最早是在 Rollup 中引入的,那么我们就以 Rollup(^2.78.0) 来举例:

  1. 新建两个文件main.mjsoutput.mjs,两个文件内容如下:
  • output.mjs

    1
    2
    export const a = 1;
    export const b = 2;
  • main.mjs:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import { a } from './output.mjs';

    console.log(a);
    const c = 3;

    function foo() {
    console.log('aaaa');
    return;
    console.log('bbbb');
    }

    foo();
  1. 执行命令rollup main.mjs --file bundle.js生成bundle.js文件,内容如下:
1
2
3
4
5
6
7
8
9
10
const a = 1;

console.log(a);

function foo() {
console.log('aaaa');
return;
}

foo();

可以看到 output 中的const b = 2;以及 main 中的const c = 3;和 return 之后的console.log('bbbb');在最终打包结果 bundle 中都被移除掉了。对比打包之前和打包之后的结果我们可以总结出 Tree Shaking 主要做了以下几点事情:

  • 消除无用的变量,函数等未被引用的内容
  • 消除代码中不会被执行的逻辑
  • 消除引入的其他模块中不被使用的内容

注意以上列举的几点是 Tree Shaking 通用的作用,但是并不是所有的打包工具都做到了这些,如默认情况下 esbuild 并不能消除掉上述示例代码中 return 之后的部分,但是可以借助工具在压缩阶段去除。

Read more

module federation

概述

前端的工程化已经持续了很长时间,从最开始的手动更新和维护,之后以文件为单位的自动化工具的产生,在开发和部署的过程中进行一系列的操作,最终组成一个较大的 SPA 或者 MPA。随着业务的发展,网页应用越来越多,以文件为单位进行构建则难以维护与复用。webpack5 引入了一个重要的特性 module federation,目的是解决多业务线,多工程下的模块复用问题。

使用方法

关于module federation的用法,官方给出了很详细的示例,我们以automatic-vendor-sharing为例简单来看一下是如何使用的:

线上 demo

app1

先看一下 app1 的 webpack 关键配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
remotes: {
app2: 'app2@http://localhost:3002/remoteEntry.js',
},
exposes: {
'./Button': './src/Button',
},
shared: {
...deps,
react: {
singleton: true,
},
'react-dom': {
singleton: true,
},
},
}),

这里的关键信息是ModuleFederationPlugin中声明了remotes,然后我们就可以使用了,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import LocalButton from './Button';
import React from 'react';

const RemoteButton = React.lazy(() => import('app2/Button'));

const App = () => (
<div>
<h1>Bi-Directional</h1>
<h2>App 1</h2>
<LocalButton />
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
</div>
);

export default App;

app2

我们回到 app1 再看一下 app1 的 webpack 配置,注意到 app1 配置了namefilename,以及exposes等信息,在开发和部署时会生成对应的文件,供其它 app 使用,我们来看一下 app2 的 webpack 关键配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
},
exposes: {
'./Button': './src/Button',
},
shared: {
...deps,
react: {
singleton: true,
},
'react-dom': {
singleton: true,
},
},
}),

app2 中也调用了 app1 的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import LocalButton from './Button';
import React from 'react';

const RemoteButton = React.lazy(() => import('app1/Button'));

const App = () => (
<div>
<h1>Bi-Directional</h1>
<h2>App 2</h2>
<LocalButton />
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
</div>
);

export default App;

如上示例所示,两个应用相互调用对方暴露出来的模块,那么扩展到多个应用也同样合适,这样就可以较好的解决模块复用问题

Read more

Webpack Tapable Source Code

Introduction

Webpack 作为主流的打包工具,提供了自定义扩展的loaderplugin,丰富了周边生态。如果想自己写一个plugin,需要对相关的hooks有所了解,而 webpack 对hooks的实现则是建立在tapable这个库上的,本篇文章希望通过对tapable源码的梳理,以加深webpack的相关知识以及部分设计模式的理解。

Read more