team
team copied to clipboard
搜狗社区搜索Preact迁移指南
背景
近期团队对部门系列产品进行一系列的性能优化,发现对于搜狗问问、搜狗指南等产品来说(搜狗百科用的是Vue.js),React显得过“重”,页面domReady时间较长;此外,由于Facebook将React协议更改为臭名昭著的“BSD+许可”协议,考虑公司利益,同时考虑到React组件化、优雅的jsx语法等诸多优点和项目、团队技术的迁移成本;因此寻找类React的轻量级解决方案逐渐提上日程。
Preact 特点
在选型的时候,主要基于以下几个考量:
- 开源社区有较多star
- 较好的性能和兼容性
- api跟React接近
- 丰富的配套框架,比如redux和router的使用
调研发现,以上几点Preact都能够很好的满足,因此最终选定为团队的类React轻量化框架进行使用和研究;
开源社区有较多star
相比于react-lite,inferno,Virtual-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在创建、更新、删除节点等操作中,有良好的表现。
首次性能测试:
此外,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-redux和react-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应用的迁移只需要很少甚至不用作改动;因此主站、指南采用途径二完成迁移,也是最理想的迁移方法。
步骤如下:
- 使用preact替换react、react-dom和react-with-addons
- 使用函数refs替换字符串refs,因为preact不支持字符串refs
- 组件创建方法全部改用ES6 Class形式,因为preact不支持createClass接口
- 在textarea,input和select封装的controlled Component中,去掉defaultChecked和defaultValue预设值;可以通过preact中LinkedState来解决
- 把 ReactDOM.render() 转换成preact的 render()
- 去掉ReactDOM相关方法调用,比如ReactDOM.findDOMNode()
- 去掉defaultProps
- 将代码中使用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>
)
}
}
参考:https://github.com/developit/preact/issues/844
再一次感谢您花费时间阅读这篇文章!祝您在这里记录、阅读、分享愉快!
转载请注明出处。
如果这篇文章对您有帮助,欢迎打赏:)
欢迎打赏