亦敌亦友的uglify

3,401 阅读3分钟

说起ugilfy,这是目前前端工程化里最重要的伙伴之一,通过对代码的压缩混淆,它给我们带来了多种好处:

  1. 压缩:减少代码量,减少网络下载时间以及浏览器的解析时间
  2. 混淆:一定程度的提升代码阅读难度,有一定程度的保护代码作用

而前端工程化的象征——webpack,从1.0时期起就内置了uglify插件,简单的配置即可使用该功能。

uglify-js

webpack 1.0~3.0均内置的插件,它伴随前端工程的服役时间最长,同时期也没有太多的竞争者,Closure一直被批评过于激进,使用者寥寥。

但是,它有一个从诞生起就伴随的缺陷,仅支持ES5代码。随着浏览器的兼容性需求越来越低,很多非现代浏览器都已经逐步被市场淘汰,chrome近几年每年都会升4个大版本。

慢慢的,原生的简洁ES6代码已经可以在浏览器里运行了,这时候uglify-js就显的有点笨重。对于ES5代码再压缩,可能也比不上直接输出ES6代码呀。

此外,还有一个危险的信号:

在前端界,大家都会去研究和讨论类库的实现,但对于工程上的工具项目,却知之甚少。对于开源项目,输入越多,它越健壮;相反的,关注的少,场景输入少,项目变得黑盒化的同时,也埋下了致命bug的种子。

uglify-es

在时代推动下,迫切需要uglify-js可以解析ES6代码,于是uglify-es诞生。webpack 4.0随着自身内部的一次大革新,将uglify-es内置,并于今年初面世。

但是。。。我用了还不到半年就遇到一个致命bug。

诡异的是,因为没有深究uglify源码,构建出的简单示例并不会出现该问题。

示例代码如下:

// Parent.jsx
import Child from './Child'

class Parent extends React.PureComponent {
  onChange = () => {
	// 触发rerender
  }
  
  render() {
	  return <div><Child onChange={this.onChange} /></div>
  }
}

// Child.jsx
class Child extends React.PureComponent {
    componentWillMount() {
        fetch('/api').then(this.props.onChange)
    }
    
    render() {
        return <div />
    }
}

最终压缩后的代码形如:

// Parent.js的render函数
return React.createElement(
    'div', 
    null, 
    React.createElement(class Child extends PureComponent {})
)

可以看到,Child由于只被一处引入,所以不再是独立模块,甚至被处理成了内联。

在js语法里这样处理,似乎没什么问题,但是当处理的是包含生命周期的组件时,就触发了严重的问题,该Child组件会被不断的销毁和创建,引起死循环。

由于部分“玄学”原因,我没找到稳定复现该bug的写法。。。

解决方式是在uglify配置中关闭reduce_vars和reduce_funcs:

uglifyOptions: {
  compress: {
    reduce_vars: false,
    reduce_funcs: false,
  },
}

这仅仅是我碰到的问题,搜一下uglify-es的bug,会发现,这项目确实有点哔——(消音),它已经从并肩作战的战友成功升级成4v6了。。。

terser

随着uglify-es不再维护(我用着的时候都不知道= =!),webpack也悄悄的更改了内置uglify依赖:

uglify-es is no longer maintained

其实还有很多项目也进行了调整:

Switch minifier from uglify-es to terser

看样子terser是从uglify-esfork出来的项目,目前看来,该项目维护者非常活跃,在很多uglify-es的bug issue下都会看到他宣传terser没有bug的身影~

希望该项目可以重新赢回开发者们的信任,让前端工程更完善,效率更高!