mopduan/team

搜狗社区搜索Preact迁移指南

Opened this issue · 0 comments

背景

近期团队对部门系列产品进行一系列的性能优化,发现对于搜狗问问、搜狗指南等产品来说(搜狗百科用的是Vue.js),React显得过“重”,页面domReady时间较长;此外,由于Facebook将React协议更改为臭名昭著的“BSD+许可”协议,考虑公司利益,同时考虑到React组件化、优雅的jsx语法等诸多优点和项目、团队技术的迁移成本;因此寻找类React的轻量级解决方案逐渐提上日程。

Preact 特点

在选型的时候,主要基于以下几个考量:

  • 开源社区有较多star
  • 较好的性能和兼容性
  • api跟React接近
  • 丰富的配套框架,比如redux和router的使用

调研发现,以上几点Preact都能够很好的满足,因此最终选定为团队的类React轻量化框架进行使用和研究;

开源社区有较多star

相比于react-liteinfernoVirtual-DOM等类React轻量级框架,Preact star数量排名第一,国内成熟的产品如腾讯QQ、花样直播等都在使用。

较好的性能和兼容性

Preact在性能方面也表现不俗。bundle在压缩后大概只有3kb,体积比React小很多,大大节省了下载和加载时间。

类React框架包大小对比:

framework version minimized size gzip size
React 0.15.6 149.74kb 46.46kb
React-lite 0.15.3 27.8kb 10.6kb
Preact 8.2.5 9.05kb 3.36kb
inferno 4.0.0-alpha1 47.94kb 8.6kb
Virtual Dom 2.1.1 45.4kb 11.8kb

在渲染性能方面,参考JS WEB FRAMEWORKS BENCHMARK系列测评文章,发现Preact在创建、更新、删除节点等操作中,有良好的表现。

首次性能测试:
results2-1024x351

此外,preact能兼容目前的主流浏览器,并且在添加polyfill的情况下,能够兼容在IE8

Api与React接近

Preact常用api基本跟React一致,这使得对React熟悉的开发者,几乎没有上手的难度,React与Preact的异同可参考官网Differences to React;如果想使用一些缺失的React Api,可以使用preact-compat,在Webpack上的external属性上作如下替换即可:

{
    resolve: {
        alias: {
            'react': 'preact-compat',
            'react-dom': 'preact-compat'
        }
    }
}

丰富的配套框架

与React配套框架react-reduxreact-router相似,Preact也有提供preact-redux和preact-router,甚至还有帮助Preact做同构直出的preact-render-to-string

Preact VS React

Preact致力于保持轻量专注,去掉React一些较占体积但“收益”较少的特性,并增加React社区呼声较高的新特性。

Preact包含的特性

  • ES6类
  • 高阶组件: 组件在render中返回其他组件
  • 无状态纯函数式组件
  • context
  • 函数refs
  • 虚拟dom比较
  • h():更为通用的react.createElement版本

新增特性

Preact 实际上添加了几个更为便捷的特性,灵感源于 React 的社区

  • props和state可以传进 render() 作为参数
  • Linked State: 输入框值、状态和state双向绑定
  • 可以使用标准的HTML属性;比如class和for
  • 批量 DOM 更新,setTimeout(1) 进行函数节流 使用 (也可以使用 requestAnimationFrame)
  • 组件和元素循环使用 / 存入池中

缺少特性

  • PropType:并非所有人使用 PropTypes,所以它们并非 preact 的核心
  • Children: 在 Preact 中并非必要, 因为 props.children 总是一个数组
  • Synthetic Events: Preact不需要过度考虑不同浏览器对事件处理的异同,所以也并没有做过度封装

其他区别

Preact与React不同,组件渲染render方法为preact库的核心方法,渲染过程不需要引入其他模块;API定义如下:

render(component, containerNode, [replaceNode])

Preact默认追加到containerNode这个DOM节点上,返回一个对渲染的DOM节点的引用。
如果提供了可选的DOM节点参数 replaceNode 并且是 containerNode 的子节点,Preact将使用它的diff算法来更新或者替换该元素节点。否则,Preact将把渲染的元素添加到 containerNode 上。

注意:
这个将来的版本可能会有小的调整,可能会改成默认替换。

从React迁移到Preact

目前问问和指南已经完成Preact迁移,迁移工作主要包含wenke工具修改和应用前端代码修改。

wenke工具修改

为了使wenke工具支持构建Preact应用,需要package.json中添加Preact库的版本依赖和Babel预编译支持,目前主站和指南均引用版本8.2.5,preset请使用babel-preset-preact

同时,preact也支持react devTools调试,只需要在入口文件头部判断当前是否为dev环境,如果是,添加如下代码即可

require('preact/devtools');

此外,在用wenke构建生产包过程中,为了避免将preact打包而导致bundle体积较大,可以在webpack的externals属性中添加preact,并在代码中从CDN中引入preact压缩包。

应用前端代码修改

首先,下载Preact开发包和压缩包并上传到CDN,在代码中引入preact,方法如下:

<script src="//cache.soso.com/wenwen/deploy/js/preact/8.2.5/preact.dev.js" data-prod="//cache.soso.com/wenwen/deploy/js/preact/8.2.5/preact.min.js"></script>

在现有的React应用中,有两种途径把 React 替换成 Preact:

  • 安装 preact-compat
  • 把 React 的入口替换为 Preact,并解决代码冲突

基于Preact的api几乎跟React的api一致,React应用的迁移只需要很少甚至不用作改动;因此主站、指南采用途径二完成迁移,也是最理想的迁移方法。

步骤如下:

  1. 使用preact替换react、react-dom和react-with-addons
  2. 使用函数refs替换字符串refs,因为preact不支持字符串refs
  3. 组件创建方法全部改用ES6 Class形式,因为preact不支持createClass接口
  4. 在textarea,input和select封装的controlled Component中,去掉defaultChecked和defaultValue预设值;可以通过preact中LinkedState来解决
  5. 把 ReactDOM.render() 转换成preact的 render()
  6. 去掉ReactDOM相关方法调用,比如ReactDOM.findDOMNode()
  7. 去掉defaultProps
  8. 将代码中使用React.createElement()方法创建虚拟dom转换成preact中的h()方法;

遇到的那些坑

1、h() 方法

import {h,  Component} from 'preact';

注意:
在preact 组件中不会显示的去用h()方法,但是在打包处理的时候会用到,所以代码检测工具会提示h方法从未被调用,但不能将其删除,如果删除掉,打包后的代码会报错。

2、refs

ref={(content)=>{this.container = content;}}

注意:
preact中ref只支持回调函数的方式;如果想继续使用字符串refs,可以在项目中引入preact-compat兼容包;

此外,强烈建议大家使用函数式refs替代字符串refs,根据react官网,字符串refs在react未来版本中也将摒弃。这里可以发现,从preact很多特性中可以看出react在未来较长一段时间的演化方向;

3、img的宽高

<div class="user-thumb-box">
    <a href="#" target="_blank" class="user-thumb getUserCard" data-uid="34755361">
        <img src="..." width="100%" height="100%" alt="头像">
    </a>
</div>

注意:
render方法里面,以上结构中的百分比会被改成0,因此不能使用百分比设置img宽高

4、render组件问题

render(<MessageBox />, document.getElementById("userNotice"))

注意:
如果没有传入第三个参数,默认会保留userNotice容器里面原有的内容,并将MessageBox追加到userNotice子节点内容中;如果想要重新渲染userNotice,将首次render返回值(userNotice虚拟dom)保存到变量root,并在再次渲染中,将root传入第三个参数中即可。

5、componentWillUnmount 生命周期函数触发方式

preact中没有显式的ReactDOM.unmountComponentAtNode方法调用,需要同过以下方式触发销毁生命周期函数:

//preact 中的render方法多次调用是向渲染元素追加,如果想替换,需要传入第三个参数。
function unmountComponentAtNode(container, child) {
    return render(<EmptyComponent />, container, child);
    function EmptyComponent() { return null; }
}
class Test extends Component{
	constructor(){
		super()
	}
	componentWillUnmount(){
		console.log('componentWillUnmount');
	}
	render(props,state){
		return <div>测试触发销毁生命周期函数的方法 {props.name}</div>
	}
}
var container = document.getElementById('#container');
var base = render(<Test name="lifei"/>,container);
// render(null,container,base)//效果如同下面封装的方法,在同一个容器中,传入第三个参数,替换原来渲染的组件为空,或者为别的组件,均会触发销毁方法
unmountComponentAtNode(container,base)

6、使用onInput回调函数代替onChange监听输入值的变化

export default class Example extends Component {
    state = {  text: '' };
    setText = e => {
        this.setState({ text: e.target.value });
    };
    render({ }, { text }) {
        return (
            <form >
                <input value={text} onInput={this.setText} />
                <button type="submit">Add</button>
            </form>
        );
    }
}

注意:
在上述受控组件中,React建议使用onChange方式监听输入值的变化并设置state;然而在preact中,onChange回调函数常常无法准确触发,官方推荐使用onInput回调方法代替onChange方法;当然,你也可以继续使用onChange方法,但需要引入preact-compat兼容包。

7、避免使用相同key的数组item放在同一个组件中

class App extends Component {
	render() {
	   return (
		<div class="App">
			{ [1, 2, 3].map(() => (<span key={ 0 }>Hello</span>)) }
			<button onClick={ () => this.forceUpdate() }>
				Rerender
			</button>
		</div>
	 );
    }
}

注意:
在react当中,我们知道在数组渲染时给每个item增加key属性,key的唯一性可以在重新渲染时提高性能,但相同key也并不会产生异常现象;然而在preact组件中,如果存在相同key数组元素,不管是在同一个数组中或者另外一个数组中,组件在更新时将重新渲染,返回新的数组组件append到container中。

解决方案:
使用数组index或Math.random()保证每个item中key的唯一性

8、dangerouslySetInnerHTML

在父组件向子组件通过props传递数据时,如果父组件中用到dangerouslySetInnerHTML,并且子组件componentDidMount中调用了setState,这时dangerouslySetInnerHTML设置的内容会丢失。
如果去掉子组件componentDidMount中的setState,dangerouslySetInnerHTML设置的内容就会显示出来。

export class Parent extends Component {
    render() {
        return (
            <Item>
                <div className="popup-prompt-txt" dangerouslySetInnerHTML={{__html: "<span>hello preact</span>"}}></div>
            </Item>
        )
    }
}

export class Item extends Component {
    constructor(props) {
        super(props);
        this.state = {
            show: false
        }
    }

    componentDidMount() {
        this.setState({
            show: true
        })
    }

    render() {
        let {show} = this.state;
        return (
            <div className="item">
                {this.props.children}
            </div>
        )
    }
}

render(<Parent/>, document.getElementById("header"))

解决办法:

const InnerHTMLHelper = ({tagName, html}) =>
    h(tagName, {dangerouslySetInnerHTML: {__html: html}});

export class Parent extends Component {
    render() {
        return (
            <Item>
                <InnerHTMLHelper tagName='div' html="<span>hello preact</span>"/>
                {/*<div className="popup-prompt-txt" dangerouslySetInnerHTML={{__html: "<span>hello preact</span>"}}></div>*/}
            </Item>
        )
    }
}

参考:preactjs/preact#844

再一次感谢您花费时间阅读这篇文章!祝您在这里记录、阅读、分享愉快!

转载请注明出处。

如果这篇文章对您有帮助,欢迎打赏:)

欢迎打赏
image