SegmentFault 西厂 XUX最新的文章
2018-10-22T10:40:34+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
在格式化的场景下,React input 的光标的处理办法
https://segmentfault.com/a/1190000016758141
2018-10-22T10:40:34+08:00
2018-10-22T10:40:34+08:00
紅白
https://segmentfault.com/u/eternalsky
15
<p>今天要来说的是有关于有数值格式化的场景中,React input 光标的一些异常的表现和对应的处理办法。故事要从一个 <a href="https://link.segmentfault.com/?enc=%2B3%2BEDEr5RJKjiSGnUQcJ%2Bg%3D%3D.qsrkMHOpcRTwIobXKPb0%2Fx9AGPafwYi4ucvJmAcR1Qq%2BrVZUTHTW0Jpp2btkGVqx" rel="nofollow">issue</a> 说起,有用户反映在使用 NumberField 组件输入时安卓下会出现光标位置异常,导致连续输入会达不到期望的结果。具体表现是什么样的呢?</p>
<p><img src="/img/remote/1460000016758144" alt="" title=""></p>
<blockquote>图1 安卓下不期望的输入行为</blockquote>
<p>可以看到,在安卓手机下每次格式化发生的时候,本来应该一直在最后的光标会错格一位,导致连续输入出现问题。而这个问题在 PC Chrome 和 iOS 上都没有出现,于是可以判定是一个兼容性问题。但这个兼容性问题是如何产生的呢?</p>
<p>分析一下格式化的话的过程,如上面的情况,输入 18758 时,因为要做针对卡号的格式化,所以会将原有的值转变为 "1875 8",从字符串长度上来看,从 5 位变成了 6 位,那么如果此时光标位置没有在值变化时跳到最后一位,则会停留在空格处,看起来就好像错格了一位,连续输入时就会有问题。</p>
<p>单从输入框的光标变化行为来看,这好像也不算是一种异常的变化,只是不响应值的变化跳到尾部而已。但引申出来的问题是为什么在 iOS 和 PC Chrome 下又会跳动到尾部呢。</p>
<p><img src="/img/remote/1460000016758145" alt="" title=""></p>
<blockquote>图2: 相同的代码在 PC Chrome 下表现与安卓不同。</blockquote>
<p>于是去网上搜索,辗转在 React 的 github 中找到这样一个 issue, <a href="https://link.segmentfault.com/?enc=6R3eLtIduEhTDrxK2TLIsw%3D%3D.XBdHMl6zB8uMSO5v%2BU0GdYJXAv9G1%2FivGhZ7WOCzyqB%2FTlFC9ZFG1FzLhH9OonFT" rel="nofollow">Cursor jumps to end of controlled input</a>。在这里 React 的主要维护者之一的 @sophiebits(spicyj) 给出了一个比较确切的<a href="https://link.segmentfault.com/?enc=kHzKReOQdoIHLJOg7J8wzg%3D%3D.OPyjvdVHIHLkv%2FED3Er6e9XlMk%2BubRucYf%2BelTnQP83BdRZPUxJMQEzZImJr8NZG7oPvgikYWFe5wXmsuJXNv%2BNlmYjZIdGUb%2B42Rz%2BJopA%3D" rel="nofollow">答案</a>。</p>
<p><img src="/img/remote/1460000016758146" alt="" title=""></p>
<blockquote>图3 sophiebits 关于 React controlled input value 变化时光标行为的解释</blockquote>
<p>原来因为 value 的变化具有非常大的不确定性,因此 React 无法使用一种可靠且通用的逻辑去保存光标的位置在一个合适的位置,因此 React 在受控模式下的重新渲染都会时光标移动到最后的位置。这个至少解释了PC Chrome 和 iOS 下光标跳动到结尾的原因,但安卓下为什么没有表现出同样的行为到目前位置我还没有找到合理的解释。</p>
<p>那有没有办法使安卓上的表现和 iOS 中一致呢?又是一阵翻阅和尝试,最后发现如果将重新渲染的过程和 input 的 onChange 置于前后两个 tick 中就可以使安卓中 input 的表现和其他平台上表现一致,即表现为光标在重新渲染时跳到最后,示意代码如下。</p>
<pre><code class="jsx">import React from 'React';
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 'xxx',
};
}
handleChange(e) {
const value = e.target.value;
// 通过 setTimeout 异步
// 使 re-render 和 onChange 处于两个 tick 中
setTimeout(() => {
this.setState({
value,
});
});
}
render() {
return (
<input
value={this.state.value}
onChange={(e) => { this.handleChange(e); }}
/>
);
}
}</code></pre>
<p>这样终于使得表现的行为在安卓和 iOS 上表现一致,并且正常输入的情况下表现得比较符合期望了,然而等等,这样就可以了吗?从之前的 React issue 中得出的结论可以看出,无论是如何的修改都会跳至 input 的结尾,这样如果是从中间修改的话会变成什么样?</p>
<p><img src="/img/remote/1460000016758147" alt="" title=""></p>
<blockquote>图4:中间编辑时又会出现问题</blockquote>
<p>从上面的图里可以看出,因为 React 无论何种修改都会将光标置尾,如果从中间进行修改,那么表现地又会很不符合用户预期,没有办法做到连续输入。这回倒是两端行为保持一致,都是不期望的状态。。</p>
<p>但是都不正常也有好处,不需要根据平台去写一些 ifelse,可以统一地去做处理。从上面的讨论中我们可以知道 React 没有保存光标的位置是因为没有一个通用并且可靠的算法去支撑这一行为。这是因为 input 的变化可能是增加空格做格式化,也可能是过滤过些字符,也可能是触发某些条件直接变成了其他字符等各种无法预测的变化行为。但是细化到数字格式化这一单一场景时,光标位置的保存逻辑就变得简单和清晰的多了。</p>
<p>在用户输入的过程中,只存在两种情况,在结尾中追加和在中间修改。在结尾追加的 case 中,例如 18758^ 时,由于一直是在向后追加的状态,我们只要一直保持光标在最后即可(即默认状态 1875 8^ ),在中间编辑的 case 下,光标并不处于结尾,如 187^5 8,此时如果在 7 后面追加了一个 8,那么理想的图标应该维持在 8 之后(即 1878^ 58),此时就应该保存光标的位置在上次 format 之前的状态。</p>
<p>逻辑清楚了,接下来就是如何实现的问题了。那么如何探测和修改光标位置呢?这就涉及了 input 中选区相关的属性,我们知道我们可以通过一些方式(如鼠标拖拽和长按屏幕等)在 input 中完成对一段话的选区,因此光标的位置其实是由选区的开始点(selectionStart)和结束点(selectionEnd)决定的。那么其实我们就可以通过读取,储存和设置这两个属性来达到我们想要实现的目的,实例代码如下。</p>
<pre><code class="jsx">class Demo extends React.Component {
...
componentDidUpdate(prevProps) {
const { value } = prevProps;
const { inputSelection } = this;
if (inputSelection) {
// 在 didUpdate 时根据情况恢复光标的位置
// 如果光标的位置小于值的长度,那么可以判定属于中间编辑的情况
// 此时恢复光标的位置
if (inputSelection.start < this.formatValue(value).length) {
const input = this.input;
input.selectionStart = inputSelection.start;
input.selectionEnd = inputSelection.end;
this.inputSelection = null;
}
}
}
handleChange(e) {
// 在 onChange 时记录光标的位置
if (this.input) {
this.inputSelection = {
start: this.input.selectionStart,
end: this.input.selectionEnd,
};
}
...
}
render() {
return (
<input
ref={(c) => { this.input = c; }}
value={this.state.value}
onChange={(e) => { this.handleChange(e); }}
/>
);
}
}
</code></pre>
<p>至此,我们终于在追加和中间编辑的情况下都实现了我们想要的效果。这是一个比较小的技术点,但是由于里面涉及了一些 React 内部的处理逻辑及平台差异性问题,排查和解决起来并不是那么容易,希望可以给有类似问题的同学在处理时有所启发。</p>
<p><img src="/img/remote/1460000016758148" alt="" title=""></p>
<h4>文中涉及的各端及浏览器信息</h4>
<ul><li>Android</li></ul>
<pre><code>Mozilla/5.0 (Linux; U; Android 8.1.0; zh-CN; CLT-AL00 Build/HUAWEICLT-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/11.9.4.974 UWS/2.13.1.48 Mobile Safari/537.36 </code></pre>
<ul><li>iOS</li></ul>
<pre><code>Mozilla/5.0 (iPhone; CPU iPhone OS 11_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15F79</code></pre>
<ul><li>PC Chrome</li></ul>
<pre><code>Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36</code></pre>
<h4>文中涉及的组件库</h4>
<ul><li>SaltUI: <a href="https://link.segmentfault.com/?enc=CrQtg5l0%2B20acieBvohwig%3D%3D.FJvlF1ZxrMEL2s8s4KI9dLopxko%2FS1XMyOeZ8Hnt2kSyFL1gDJgWEeXHmGz0%2F6Ct" rel="nofollow">https://github.com/salt-ui/sa...</a>
</li></ul>
今天,你的浏览器 “滚动” 了吗?
https://segmentfault.com/a/1190000010594249
2017-08-10T14:42:42+08:00
2017-08-10T14:42:42+08:00
紅白
https://segmentfault.com/u/eternalsky
9
<h2>今天,你的浏览器 “滚动” 了吗?</h2>
<h3>序</h3>
<p>在 Web 页面中,一个有高度或者宽度的容器是最常见的构成元素,而在其中的子元素有很大的概率超过父容器的尺寸限制,我们称之为“溢出”。而应对“溢出”,隐藏或者滚动是最常见的处理方式。滚动,作为 FEers 最经常处理的一种行为,却因为不同浏览器的各种表现形式让大家头痛不已,今天笔者从自身维护的组件出发,和大家分享一下自己在处理滚动和滚动条时遇到的问题,以及解决的办法,希望能够给你在解决同类问题时带来一些启发。同时本文也是 “从零开始的 React 组件开发之路” 系列的第二篇 - 表格篇番外。</p>
<h3>遇到的问题 1:尴尬的双滚动条</h3>
<p>笔者在团队中负责基础组件的开发和维护,作为一个 B 类业务较多的团队,表格是最常用和需求最为旺盛的组件,假设有下方这样一个最简单的表格结构。</p>
<p><img src="/img/remote/1460000010594252" alt="" title=""></p>
<blockquote><p>图1:最简单的表格结构</p></blockquote>
<p>因为空间有限,我们希望表格高度限定,这样势必引入表格上下滚动的情况。同时,为了查看的方便,我们希望表格头不会一起滚动,即表格头需要固定,只有表格体滚动,因此我们需要把表格头和表格体放入两个容器中,而只让表格体的容器滚动。这是很普通的需求,也很容易实现,到目前为止一切都很顺利。</p>
<p><img src="/img/remote/1460000010594253" alt="" title=""></p>
<blockquote><p>图2:表头固定,表格体滚动</p></blockquote>
<p>然而这一切的美好,随着表格列数的增多,变的有了一点乌云。因为页面宽度受到电脑屏幕的限制,我们往往对表格的宽度也有限制,不可能无限延展开。那么如果有很多的列呢?显然,让表格左右滚动是一个很自然的想法。由于我们的表头和表格体在两个不同的容器中,让这件事变的稍微麻烦一点。关于如何让表头和表格体同步左右滚动,不是这篇文章讨论的重点,所以不做详细讨论,简单来说,我们通过监听横向滚动的事件和不断获取当前的 <code>scrollLeft</code> 来获得同步。有一个麻烦的点是,我们不希望只能通过滚动表格体来实现表格滚动,也希望可以通过滚动表头来实现表格的左右滚动,这就要求必须设置表头的容器为 <code>overflow-x: auto</code>。</p>
<p><img src="/img/remote/1460000010594254" alt="" title=""></p>
<blockquote><p>图3:由于表格头和表格体都需要横向滚动,会引入两个滚动条。</p></blockquote>
<p>这显然突破了大多数人对表格的认知,横向滚动会有两个滚动条,一点都不美观,需要我们在这个基础之上进行优化。表格体上的横向滚动条是没有问题的,主要问题在于表格头的,我们既希望能够横向滚动,又不想看到那个该死的滚动条,怎么办呢?想办法隐藏掉他就好了!首先我们设置表格头的容器 <code>overflow-x: scroll</code> 以保证无论是否需要横向滚动都会出现滚动条,方便我们简化状态的判断。接下来我们可以再设置表格头的容器 <code>margin-bottom: -scrollBarWidth</code> 来隐藏让他的父级帮忙吞掉这个滚动条,一切就大功告成了。但令人头大的是,滚动条的尺寸在不同浏览器,甚至是不同系统(例如 Windows 和 Mac 下的 chrome)中都是不一样的! 我们无法很暴力地通过制定一个固定的值来做这件事,因此我们需要在表格渲染到页面上去之后,主动去探测滚动条的宽度。</p>
<pre><code class="js">const scrollbarMeasure = {
position: 'absolute',
top: '-9999px',
width: '50px',
height: '50px',
overflow: 'scroll',
};
let scrollbarWidth;
const measureScrollbar = () => {
if (typeof document === 'undefined' || typeof window === 'undefined') {
return 0; // 如果 document 不在,则证明不在浏览器环境,直接返回,兼容 node server render。
}
if (scrollbarWidth) {
return scrollbarWidth; // 滚动条在固定的环境下宽度不会改变,因此只做一次探测即可,优化性能。
}
const scrollDiv = document.createElement('div');
Object.keys(scrollbarMeasure).forEach((scrollProp) => {
if (Object.prototype.hasOwnProperty.call(scrollbarMeasure, scrollProp)) {
scrollDiv.style[scrollProp] = scrollbarMeasure[scrollProp];
}
}); // 创造一个远离人世的带滚动条的 div 用于探测,用户对于此无感知。
document.body.appendChild(scrollDiv);
const width = scrollDiv.offsetWidth - scrollDiv.clientWidth; // 获取滚动条的宽度,offsetWidth 和 clientWidth 的区别,你能说清楚吗?
document.body.removeChild(scrollDiv); // 探测完成,销毁测试元素,减少对页面的影响。
scrollbarWidth = width; // 缓存结果,优化性能
return scrollbarWidth;
};
</code></pre>
<h3>遇到的问题 2:对不齐的表头和数据</h3>
<p>通过上面的方法,我们成功地隐藏了表格头的横向滚动条。稍微滚动一下,一切正常,一切都按照预想的执行,直到一直滚动到头,问题出现了,最后一列的表头和下面的数据居然是对不齐的!!</p>
<p><img src="/img/remote/1460000010594255" alt="" title=""></p>
<blockquote><p>图4:当表格体又可以左右滚动时,问题开始复杂起来~</p></blockquote>
<h4>这是怎么回事呢?</h4>
<p>原来,因为我们允许表格体上下滚动,使得在容器右侧出现了一个纵向的滚动条。而表头因为我们希望他是固定的,因此放在了另一个容器中,这导致他不能共享这个滚动条。因此,这使得表格体拉到头的时的 scrollLeft 也正好是表格头到头的位置,两个到头的位置上差了一个滚动条的宽度!看到这里,也许有的小伙伴可能会开始自己操练起来看看是不是这样,然后发现并没有类似问题,大呼坑爹,他们看到的情况大致如下图:</p>
<p><img src="/img/remote/1460000010594256" alt="" title=""></p>
<blockquote><p>图4:Mac 某些设置下,看到的是另一份景象。</p></blockquote>
<p>这引出了一个 Mac 下一个比较好玩的小设置,在 Mac 下滚动条何时显示也可以配置,大致分为三类,具体的配置可以在 <code>系统偏好设置 -> 通用</code> 中看到。</p>
<p><img src="/img/remote/1460000010594257" alt="" title=""></p>
<p>当我们选择 <strong>滚动时</strong> 的时候,只有当我们滚动一个元素的时候才会显示滚动条,且这个滚动条是飘在内容上,不会占据体积,于是便能看到 图3 中的情景。这个兼容性上升到了系统的程度,在 PC 上还是比较少见的(笑)。</p>
<p>所以这个问题只会在这种情况下没有出现,在 Mac 下选择 <strong>始终显示</strong>,也同样会出现。</p>
<h4>那么如何解决这个问题呢?</h4>
<p>其实,从上面的分析中我们也可以也大致地找到了问题的根源,<strong>表头没有共享表格体的纵向滚动条</strong>,那么我们只要想办法解决这个就好了。这个说起来简单,但毕竟不是在同一个容器中,如何共享呢?方案一:插入一个和滚动条相同宽度的 dom 元素充当滚动条,但这会引起另一个问题就是表格头和表格体的实际宽度不同,在各种滚动计算上引发很多麻烦。方案二:滚动条虽然在不同系统、不同浏览器里都不一样尺寸,但却有个优点就是,只要在同一系统、统一浏览器里不管因为什么原因,出现在什么地方,他的尺寸总是保持一致的。利用这个特点,我们可以设置表头容器的 <code>overflow-y: scroll</code>,总是包含一个纵向滚动条的道,这样就兵不血刃的解决了这个麻烦的问题。</p>
<p><img src="/img/remote/1460000010594258" alt="" title=""></p>
<blockquote><p>图5:利用空的滚动条连接下面的滚动条来就可以解决上面的问题。</p></blockquote>
<h4>但这样又引入了新的问题</h4>
<p>这样虽然比较好的解决了表格体有滚动条的情况,但是如果表格体没有滚动的情况下,遇到的问题就正好逆转了,又会出现对不齐的情况!</p>
<p><img src="/img/remote/1460000010594259" alt="" title=""></p>
<blockquote><p>图6:当表格体没有纵向滚动条的情况下,又会出现新的问题。</p></blockquote>
<p>解决这个问题也有几种思路,简单一点的思路可以模仿上面,给表格体也设置 <code>overflow-y: scroll</code>,这样不管是否有滚动,看起来都是一样而且不会错位了。但是这种方法并不十分美观,尤其在 windows 下显得比较丑陋。于是在此方案基础之上,我们加入了对于表格体纵向滚动的检测,当滚动区域的高度不大于容器高度时,即可认为没有滚动,此时同时设置表头和表格体 <code>overflow-y: hidden</code> ,就可以达到解决上述问题,而在没有滚动的情况下也保持美观的要求。</p>
<h3>遇到的问题3:列固定下的整体横向滚动</h3>
<p>解决了上面两个问题之后,一个完整的支持表头固定和横向滚动的表格就完成了。接下来又有了新的需求,要在原有表格基础之上,支持左右侧列固定。这也是表格中比较常见的需求,一些数据列或者操作列处于高频使用下,希望能够固定,这时就产生两种处理表格体横向滚动条的方式。</p>
<p><img src="/img/remote/1460000010594260" alt="" title=""></p>
<p><img src="/img/remote/1460000010594261" alt="" title=""></p>
<blockquote><p>图7:支持左右列固定后的方案 A 和 B</p></blockquote>
<p>方案 A 模仿表头的设计方案,将固定的列放在另一个容器当中,把需要滚动的列单独放置在一个可以滚动的容器当中。这种方案的问题在于,固定列的宽度和列数都不像表头一样固定且较小。当固定区域很大的时候,会严重挤压中间滚动区域滚动条的可操作区域,影响用户体验。同时他也会遇到和表头一样,滚动至最后一行对不齐的情况。方案 B 则比较好的解决了方案 A 的第一个问题,左右侧漂浮在表格的左右两边,覆盖对应的固定区域,横向滚动条可以覆盖整个表格,不会受到固定列宽度的限制。</p>
<h4>方案 B 虽然好,但是仍有两个问题需要解决</h4>
<ol>
<li>如何实现在固定区域也可以做到整个表格上下滚动</li>
<li>漂浮的固定列会遮盖住全局的横向滚动条。</li>
</ol>
<p>对于问题 1,他的解决思路其实和刚才表头的解决思路类似。主要是通过适当地设置 margin 来隐藏本应存在的滚动条,这里不再赘述。 <br>对应问题 2,如果是没有纵向滚动,即 <code>height: auto</code> 这样的情况下经过测试不存在这个问题,float 的元素会很自然地不占用滚动条的体积,因此不会有遮挡。如果是有纵向滚动的,情况则复杂了一些,当 float 的元素和下面的主表格等高时会出现遮挡的情况。</p>
<p><img src="/img/remote/1460000010594262" alt="" title=""></p>
<blockquote><p>图8:方案 B 引入的遮挡滚动条的问题</p></blockquote>
<p>那既然同高会有遮挡的问题,只要我们对应的减掉对应的滚动条高度就可以了,解决第一个问题时我们得到的大杀器,获取滚动条的高度,也可以用在这里。这个方案也能很好地应该对 Mac 下有的设置不显示滚动条的情况,在不显示滚动条的情况下,我们获取到的宽度是 0,即没有影响。而滚动时,滚动条会自动浮在最高的位置,因此仍然整条可见。</p>
<h3>总结</h3>
<p>那么,今天,你的浏览器 “滚动” 了吗?你是否已经笑对其中了呢~ </p>
<p>本文从实际的组件需求出发,通过三个有关滚动条的问题出现和解决为线索,和大家分享了如何跨系统、跨浏览器地兼容滚动尤其是滚动条的问题,虽然是以表格为核心阐述,但解决方案不局限于表格之中,希望能给大家在遇到类似问题时提供一些灵感。文中涉及的行列固定相关的知识,因为非本文重点,故一笔带过,如果有兴趣了解实现详情,可以参考我们团队开源的 PC 端 UI 组件库 <a href="https://link.segmentfault.com/?enc=71hu0DZlRKvC6pTkJkbaFQ%3D%3D.%2BJruSl32jkP1vPx4EWgyTnakGivn182h55pFHtTSqFosoBryXb0Ogyv%2BlFgsFBo%2F" rel="nofollow">UXCore</a> 和对应的组件 <a href="https://link.segmentfault.com/?enc=zYO7RjEb4%2Fwzp81DQcWdUA%3D%3D.XFUSoVTYsAHY48Lo6CB1xJDahSuYWl7SkW5qACXbUslzqAplmZrFEdtYjs4aw6fF" rel="nofollow">Table</a> 里的代码~</p>
<p><img src="https://segmentfault.com/image?src=https://gw.alicdn.com/tps/TB1TVapKFXXXXbbXpXXXXXXXXXX-1000-500.png&objectId=1190000006201488&token=ab294cff2351c1260dca388f99ab72c3" alt="" title=""></p>
<p><img src="/img/remote/1460000010594263" alt="" title=""></p>
Nowa 上手篇(4)巧用命令集
https://segmentfault.com/a/1190000009661892
2017-06-05T16:09:13+08:00
2017-06-05T16:09:13+08:00
Poling
https://segmentfault.com/u/poling
0
<p>本系列文章,不断更新中...</p>
<ul>
<li><p><a href="https://segmentfault.com/a/1190000009278983">Nowa 上手篇(1)- 介绍</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009279332">Nowa 上手篇(2)- 创建 React Web 项目</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009330713">Nowa 上手篇(3)- 工具使用指南</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009661892">Nowa 上手篇(4)巧用命令集</a></p></li>
<li><p>...</p></li>
</ul>
<p>这篇文章着重介绍 Nowa 的命令集功能。命令集的功能是可视化版本才有的特色功能,毕竟在命令行里,只需要 <code>npm run xxx</code> 就行了。</p>
<p><img src="/img/bVOHD2?w=2028&h=1328" alt="clipboard.png" title="clipboard.png"></p>
<p>命令集里面的所有命令来源于导入项目里 <code>package.json</code> 文件中的 <code>scripts</code> 字段。当然 <code>start</code> & <code>build</code> 命令已经挪到头部的基本操作栏了。</p>
<p>那么,Nowa 里面的命令集有什么特殊的地方呢?</p>
<h2>特点1: 可视化的命令操作</h2>
<p>也许笔者坐井观天,目前没有见过其他工具把 <code>package.json</code> 可视化得如此彻底。</p>
<p>用户只需要点击每条命令后面的启动按钮就能运行该条命令,不需要输入 <code>npm run</code>。</p>
<p>当命令启动后,图标会变成 <strong>暂停</strong> 态,当命令自动结束后,会重新变成 <strong>启动</strong> 态。如果命令需要手动退出才能停止,那么用户直接点击暂停按钮就能结束命令运行了,而不是 (CTRL + C) * 2。</p>
<p>点击删除按钮同时会把这条命令从 <code>pacage.json</code> 中移除,所以 confirm 的时候要小心。</p>
<p>如果需要增加一条命令,用户可以点击添加按钮添加新命令。当然, Nowa 会同时把命令写入到 <code>package.json</code> 文件里。</p>
<h2>特点2: 输出窗口切换</h2>
<p>在命令行模式下,可以开启多个终端 tab 启动不同的命令达到命令输出分流的目的,但是在 windows 里面就麻烦了,因为 cmd 没有多 tab 的支持。</p>
<p>所以 Nowa 人性化的提供了这个特性,用户点击命令集的命令名字,输出区域会直接显示该条命令的输出,不会和其他命令输出重叠。</p>
<p>而且输出区域一旦有内容,就会出现清理按钮,用户可以删除这些输出。</p>
<p>如果用户觉得输出区域太小,可以点击放大按钮隐藏命令集块达到放大输出区域的目的。当然,全屏工具也是可以的。</p>
<h2>特点3: 全局命令设置</h2>
<p>点击命令集旁边的设置按钮可以进入全局命令设置页面。</p>
<p><img src="/img/bVOHEn?w=2028&h=1328" alt="clipboard.png" title="clipboard.png"></p>
<p>在这里配置的命令一旦开启 <strong>应用</strong>,那么当前导入的项目中都会含有这条命令。而且之后导入的项目也会包含这条全局命令。</p>
<p><img src="/img/bVOHEC?w=442&h=436" alt="clipboard.png" title="clipboard.png"></p>
<p>如果关闭 <strong>应用</strong>,那么当前导入的项目中会删除这条命令。</p>
<p>如果用户的命令集中已经包含同名的命令了,那么全局的命令不会覆盖项目已有的命令,以此保证项目中的命令优先级最高。</p>
<p>那么这个全局命令设置有什么用呢?</p>
<p>其实这个对笔者自己用处不大,但是可以配置一些通用的命令,比如 commit 啥的,不用重复写了。</p>
<p>这个全局命令设置的功能在 1.7.3 版本里面才有哦~ 赶紧去<a href="https://link.segmentfault.com/?enc=CeTZXCJM5lWA2iPciJ%2F02w%3D%3D.Oz1cVke16b%2BK%2FYSyBKtVQGq%2FRKcP%2BBMddntlGxPcFeI%3D" rel="nofollow">官网</a>下载新版更新吧!</p>
Electron 桌面应用开发系列文章 - 减小应用的打包体积
https://segmentfault.com/a/1190000009366425
2017-05-10T17:42:39+08:00
2017-05-10T17:42:39+08:00
Poling
https://segmentfault.com/u/poling
9
<h2>前言</h2>
<p>笔者最近一直在使用 electron 开发一个可视化工具 <a href="https://link.segmentfault.com/?enc=JQb1NUq4k%2BYJBNWrXNvMhA%3D%3D.AswFcE7lKVdhclmMn0tKHPZNAVoDsKSbrK6llamRBx0%3D" rel="nofollow">Nowa</a>,里面的技术栈是</p>
<ul>
<li><p>webpack2</p></li>
<li><p>babili</p></li>
<li><p>react</p></li>
<li><p>electron</p></li>
<li><p>electron-builder</p></li>
</ul>
<p>使用过 electron 的人都知道,打出来的包是很大的,因为electron 内置了 Node & Chromium, 所以啥都还没干,打出来的应用安装包就有几十兆了。</p>
<p>无法在 electron 上做文章,那么只好在 webpack 打包程序代码的过程中捣鼓了。以前打包应用的时候,程序里会有 <code>node_modules</code> 文件夹。这次任务就是干掉这个文件夹。</p>
<h2>目录结构</h2>
<p><img src="/img/remote/1460000009437306" alt="ele0" title="ele0"></p>
<p>大家会发现这里居然有两个 <code>package.json</code> !! 其实主要是因为 electron-builder 的 <a href="https://link.segmentfault.com/?enc=RzKF5DNvAsi6vXP59N7J5g%3D%3D.M3jAp3M3RfJ%2BQ3MBGh%2BjiWbuLMAUgn0pF713KjVz7dO33qTa%2FY34jZ%2Bh1YMCD17bgY5PdI%2Biq%2B4kbm2w9gWeZVSkQOzVO4O7bg056AfEZdqgskeUQNpmU6DXri5MEKFw" rel="nofollow">Two package.json Structure</a> 的设置。把打包需要的依赖与开发依赖完全分开,纯粹打包你想要的东西,所以设置了 app 文件夹放这些。</p>
<p>electron-builder 只会对 app 文件夹进行打包,换句话说,这里面有多少东西就会打包多少内容。</p>
<p>所以我们可以想法设法减少不必要的东西。比如这里没有任何依赖, <code>node_modules</code> 是空的!</p>
<h2>打包过程干货</h2>
<p>结合 electron 的特殊环境,webpack 编译过程有很多文章可以做。</p>
<h4>1、 考虑 electron 的 Chromium & Node 版本</h4>
<p>在 webpack 打包的时候,我们抛弃低版本浏览器的那些兼容,因为我们只用 Chromium,所以不必要的会增加编译输出的 preset 就不要了,比如 <code>loose</code>,和一些 shim。</p>
<p>而且高版本的 node 已经支持一些 es6 的属性了,我们真的需要降级到 es5 么?当然不是的。</p>
<p><strong>A、 修改 babel 配置</strong></p>
<p>推荐使用 <code>babel-preset-env</code>设置。这个 preset 主要可以设置项目当前的环境,适时进行引入新特性,如果对其没有其他配置,就相当于使用了 <code>babel-preset-latest</code> 。</p>
<pre><code class="json">// .babelrc
{
"presets": [
["env", {
"targets": {
"electron": 1.6,
},
"loose": false,
"modules": false,
"useBuiltIns": true
}],
"stage-2",
"react"
],
"plugins": [
]
}</code></pre>
<p>demo 里面设置了 <code>targets</code> 是 electron 1.6 版本,如果不嫌麻烦的话,可以根据当前的 electron 的 node 和 chrome 版本进行分别设置。<br>比如:</p>
<pre><code class="json">
"targets": {
"node": 7.4,
"chrome": 56
}
</code></pre>
<p><strong>B、 更换压缩方案</strong></p>
<p>通常我们以前的打包方案是这样子的:</p>
<blockquote><p>ES2015+ code -> Babel -> Babili/Uglify -> Minified ES5 Code</p></blockquote>
<p>现在,我们可以不用降级这么多,使用一个工具<a href="https://link.segmentfault.com/?enc=IhmySmq9TpfYatOtvjPl%2Fw%3D%3D.m0kvlLuoqlcH33JNW2BeGmtiATO5IsuFQ5sUmCA55pGcn6wASoQrYwLS0qncC2FN" rel="nofollow">babili</a>(不要看成bilibili),它是 babel 的压缩工具。</p>
<p>babili 的打包方案是这样的:</p>
<blockquote><p>ES2015+ code -> Babili -> Minified ES2015+ Code</p></blockquote>
<p>它不会编译成 es5 的版本,而是对当前版本进行压缩。这简直就是 electron 的绝配啊。</p>
<p>为了能在 webpack 中使用,我们需要引入一个插件 <code>babili-webpack-plugin</code>。 这个是使用于生产环境的,所以我们 webpack 生产环境配置中可以这样引入:</p>
<pre><code class="js">// webpack.prod.config.js
const BabiliPlugin = require('babili-webpack-plugin');
module.exports = {
...,
plugins: [
...,
new BabiliPlugin()
]
}</code></pre>
<h4>2、 对 main 端代码进行打包</h4>
<p>通常我们可能不对 main 端进行打包,我之前做的项目就没打包,main 端的依赖全部都合入安装包去了。如果 main 端依赖很大的话,那真是灾难。</p>
<p>实际上 main 端也能进行打包,与 renderer 端一样,输出到 <code>app</code> 目录,这样 node_modules 就空了。</p>
<p>然而,如果有引入第三方的 native node 模块的话,笔者没有尝试过是否能行得通,猜测很可能还是要放到 <code>node_modules</code> 里面保险。有尝试过的看官请留言。</p>
<p>对 main & renderer 端打包代码的时候,要注意设置 webpack 的 <code>target</code> 字段。</p>
<pre><code class="js">// renderer.webpack.config
{
target: 'electron-renderer'
}
// main.webpack.config
{
target: 'electron-main'
}</code></pre>
<p>webpack 的 target 默认是 <code>web</code>。如果你没有进行更改的话,renderer 端就无法使用 node 模块了。</p>
<h5><strong>main 端注意事项</strong></h5>
<p>对 main 端打包的条件是有些条件的。</p>
<ul>
<li><p>如果说您使用了<code>remote.require(xxx)</code> 的方式在 renderer 端引入了 main 端需要的模块,那么您需要在 app 目录下放该模块。</p></li>
<li><p>如果在 main 端调用了 <code>child_process</code> 的方法去执行放在 app 文件夹里面的js文件,而这些脚本依赖了非 node 原生模块的时候,请把这些模块安装到 app 里面的 node_modules 里面。</p></li>
</ul>
<h5><strong>main 端遇到的问题</strong></h5>
<p>main 端打包容易碰到如下问题:</p>
<p>依赖中出现 <code>#!/usr/bin/env node</code> 这样的语句或者包含了 <code>*.node</code> 的脚本,这个使用您需要使用一些特殊的 loader 进行处理。</p>
<pre><code class="js">{
test: /\.js$/,
include: /node_modules/,
loader: 'shebang-loader'
},
{
test: /\.node$/,
include: /node_modules/,
loader: 'node-loader'
}</code></pre>
<h4>3、合适的renderer 端构建方案</h4>
<p>笔者在renderer 端构建采用了 DLL(动态链接库)方案, 也是 webpack 官方比较推荐的方案。它可以快速的提升构建速度,特别是明显的提升第一次启动的速度。在生产环境就不要使用它了,因为 dll 文件的体积比较大。<br>css 要使用 <code>ExtractTextPlugin</code> 与 js 代码分离开来,不要合并,不要合并,因为文件体积同样比较大。</p>
<h4>4、 注意 electron 版本号 和 electron-builder 版本号</h4>
<p>使用新的 electron 版本打包出来的安装包会比旧版本大几兆,其实很容易理解。</p>
<p>使用不同版本的 electron-builder 打包出来的也不同。大于 13.* 版本的打包出来的安装包同样大几兆。</p>
<p>几兆到底是几兆呢? demo 的例子实测是 3~5 MB。如果大家不care这几兆的话其实无所谓。</p>
<p>为了减小安装包体积,笔者真是无所不用其极。</p>
<p>如果大家有更好的打包方式,请评论回复。</p>
<h2>参考</h2>
<ul>
<li><p><a href="https://link.segmentfault.com/?enc=9%2FdpGpJ6MLFodwlczlQObQ%3D%3D.ADms72V%2FTLFlA5a2EazSvnGQVsoYy2BR4YuyFCijpXysYPeyTOIHoo9YaGuxrJTxgl%2BQn9YzpiEYQp0CowOaGg%3D%3D" rel="nofollow">https://github.com/chentsulin/electron-react-boilerplatehttps://github.com/chentsulin/electron-react-boilerplate</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=1GsKqbhwP%2FKpIWTldjNGdQ%3D%3D.3wRds7WUNclGfzQJEJqJmXjoSYN9zqp%2FQRviastmiOb7VP3lU43wdZH3ewYY7Vxm" rel="nofollow">http://babeljs.io/docs/plugins/preset-env/</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=rs%2Fv3AKOZB9%2FTdWslIK40w%3D%3D.0Z3RT4aY3HKjy8LZOJLTy3ajsisbym%2BcFc0QM2p0ghh9nugwVxkQ%2FQG0sQe9cCGt" rel="nofollow">https://babeljs.io/blog/2016/08/30/babili</a></p></li>
</ul>
Nowa 上手篇(3)- 工具使用指南
https://segmentfault.com/a/1190000009330713
2017-05-08T12:49:51+08:00
2017-05-08T12:49:51+08:00
Poling
https://segmentfault.com/u/poling
2
<p>本系列文章,不断更新中...</p>
<ul>
<li><p><a href="https://segmentfault.com/a/1190000009278983">Nowa 上手篇(1)- 介绍</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009279332">Nowa 上手篇(2)- 创建 React Web 项目</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009330713">Nowa 上手篇(3)- 工具使用指南</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009661892">Nowa 上手篇(4)巧用命令集</a></p></li>
<li><p>...</p></li>
</ul>
<p>这一篇文章主要是 Nowa 工具的使用指南。</p>
<p><img src="/img/bVNjuO?w=2024&h=1324" alt="project" title="project"></p>
<p>这么美丽的脸蛋就这样被我划花了。</p>
<h3>创建项目</h3>
<p>请参考 <a href="https://segmentfault.com/a/1190000009279332">Nowa 上手篇(2)- 创建 React Web 项目</a></p>
<h3>导入项目</h3>
<p>请直接把项目文件夹拖入工具,或者点击项目列表的文件夹图标。</p>
<h3>基础操作</h3>
<p><img src="/img/bVNjuV?w=842&h=228" alt="基础操作" title="基础操作"></p>
<p>基础操作包含7个小功能:</p>
<p>1、 启动项目</p>
<p>即运行 <code>nowa server</code>,nowa 项目会自动执行 webpack 任务。</p>
<p>2、 浏览器打开项目访问地址</p>
<p>该功能只在 nowa 项目启动之后才能生效。</p>
<p>3、 编译项目,</p>
<p>即运行 <code>nowa build</code>,执行打包任务。</p>
<p>4、 使用编辑器打开项目</p>
<p>默认 Sublime Text 打开项目。如果打开失败请用户前往设置页面设置编辑器路径。</p>
<p>5、 唤起终端</p>
<p>在项目路径下打开系统终端,即 cmd & terminal。</p>
<p>6、 打开项目文件夹</p>
<p>7、 删除项目</p>
<p>只在工具中移除项目,不会物理删除项目。</p>
<p>奇怪,我怎么没看到最后两个功能的按钮呢?`基础操作`框起来的明明就是5个功能!不要着急,不要着急,最后两个功能隐藏在项目列表每条项目的向下箭头那,图片里已经圈出来了。</p>
<p>更多详细解释请前往 <a href="https://link.segmentfault.com/?enc=9gHBApAprTzDvXU2bde6xA%3D%3D.hWxnkqwUTFJHzbv%2FGQ2LvGqIZQuBmCeQDeTJIgk50oypykoYaIy5DrSy4DwlNZqjqIp3uQTPgDXGSDYLEPlIug%3D%3D" rel="nofollow">官方使用文档-基础操作</a>。</p>
<h3>项目功能模块</h3>
<p>项目功能模块主要针对项目的 <code>package.json</code> & nowa 配置文件进行修改。</p>
<p>包含3个功能:</p>
<p>1、 控制台</p>
<p>默认界面就是控制台界面。</p>
<p><code>命令集</code> 对应 <code>package.json</code> 的 <code>scripts</code> 字段,用户只需要点点按钮就能直接运行 npm 命令,不要每次在终端进行输入。点击每条命令名称,可以切换到该命令的控制台输出。</p>
<p>更多详细配置可以看此篇文章<a href="https://segmentfault.com/a/1190000009661892">《Nowa 上手篇(4)巧用命令集》</a>。</p>
<p>2、项目设置</p>
<p><img src="/img/bVNju1?w=2024&h=1324" alt="项目设置" title="项目设置"></p>
<p><code>基本设置</code> 主要针对 <code>package.json</code> 进行配置,里面的字段相信大家很熟悉。</p>
<p><code>开发配置</code> 和 <code>编译配置</code> 只针对 nowa 项目才有的。</p>
<p>大家发现了没有,潜意思是用户可以导入非 nowa 项目。</p>
<p>更多详细解释请前往 <a href="https://link.segmentfault.com/?enc=wFpFYHeYmSB5OkbpU1NXQg%3D%3D.m82mqX298KBFqNw0Y4TIQEw7I9XFuesfgN2GVmlQ8dqFfXbVphNWPf1%2FmZKeG8Lq5I0WJ4XTXJ6%2BNiptiIuSUw%3D%3D" rel="nofollow">官方使用文档-项目设置</a>。</p>
<p>3、依赖管理</p>
<p>这是鄙人继 <code>命令集</code> 之后的第二个喜欢的功能。</p>
<p><img src="/img/bVNjvd?w=2028&h=1328" alt="依赖管理" title="依赖管理"></p>
<p>看图就很明显,这里是用来管理 <code>package.json</code> 的依赖滴。用户可以在此安装、删除、更新、查找依赖。如此方便的可视化依赖管理功能真是让人欲罢不能。</p>
<p>更多详细解释请前往 <a href="https://link.segmentfault.com/?enc=mCp6e5%2BYwZURHgkRFOOQFw%3D%3D.y1MZWYSrSCx0Y1XD6JR4msuZtRupTP56szn6ceMRc%2BKIjwizUL17pDewJT65%2Feukt9Isi4zB6r3zWBIgPo5FcA%3D%3D" rel="nofollow">官方使用文档-依赖管理</a>。</p>
<h3>工具设置</h3>
<p><img src="/img/bVNjvj?w=2028&h=1328" alt="工具设置" title="工具设置"></p>
<p>这个页面主要对工具进行全局设置,请注意,这里是工具,这里是工具,不是项目。</p>
<p>眼不瞎的读者们已经发现,前面的大图里面文字语言已经进行更换了。国际化的功能就是在这里设置了。</p>
<p>为什么会有英文版呢?虽然 nowa 目前使用人群没有歪果仁,但是为了更好的提升逼格,所以硬塞入国际化功能。</p>
<p>同样,在这里可以设置编辑器路径,目前就支持仨儿,Sublime Text, VScode, WebStorm。</p>
<p>更多详细解释请前往 <a href="https://link.segmentfault.com/?enc=yT%2FhGMTKpDz4c4yOAvE9SA%3D%3D.THYDWKXCDeFwCDhbz%2BGP9Houf3%2Fq9E3PFv32VpnOr2rMrVlkiqcZWD2objqVbH%2BSs7PvuxLsutTKook92xnY3g%3D%3D" rel="nofollow">官方使用文档-工具设置</a>。</p>
<h3>其他小功能</h3>
<p><img src="/img/bVNjvm?w=530&h=160" alt="其他" title="其他"></p>
<ul>
<li><p>如果有啥想要比较实时的回复请提<code>反馈</code></p></li>
<li><p>如果有星星点点的建议请一定要提<a href="https://link.segmentfault.com/?enc=RqTJrV%2BgKUwWur%2FnANDxGQ%3D%3D.d7z%2F6%2FNGUTaumuxTXwswic8eGxRevvbdQNjliAZWo%2BUqra%2BOs9AVxUqR3rJhwkghduUGDTD9oJYy3Uu%2BQSb2Yw%3D%3D" rel="nofollow">issue</a></p></li>
<li><p>如果有任何不明白的,请看<a href="https://link.segmentfault.com/?enc=QG7TjlGIdkFyiz7NRg%2B1IQ%3D%3D.qGY09bgNQ7of5uiVxdtL7%2FdLzbS1zW8VVn55oDclzCKdRyGVQEHBw5mLoHyH65zZzT86jg%2BkFmEbnNiKYPoNuQ%3D%3D" rel="nofollow">帮助文档</a>,当然,您也可以直接在此文下评论。</p></li>
</ul>
<p>谢谢大家,指南就到这里了,欢迎大家前往<a href="https://link.segmentfault.com/?enc=xPBp5WU0nTb8ntGHlMOP0A%3D%3D.IEsE5u9kjWpShDVvZ5LaNikSjZFfacRX0%2FZzlnBW1Sw%3D" rel="nofollow">官网</a>下载工具一看究竟。</p>
Nowa 上手篇(2)- 创建 React Web 项目
https://segmentfault.com/a/1190000009279332
2017-05-03T17:40:29+08:00
2017-05-03T17:40:29+08:00
Poling
https://segmentfault.com/u/poling
1
<p>本系列文章,不断更新中...</p>
<ul>
<li><p><a href="https://segmentfault.com/a/1190000009278983">Nowa 上手篇(1)- 介绍</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009279332">Nowa 上手篇(2)- 创建 React Web 项目</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009330713">Nowa 上手篇(3)- 工具使用指南</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009661892">Nowa 上手篇(4)巧用命令集</a></p></li>
<li><p>...</p></li>
</ul>
<p>在看完废话超多的<a href="https://segmentfault.com/a/1190000009278983">介绍篇</a>后,欢迎各位看官来到实战篇,当然没看介绍也完全 OK 啦。</p>
<p>这篇文章主要介绍如何使用 Nowa 可视化工具快速创建 web 项目。好了,废话不多说,卷起袖子,开战~</p>
<h2>1、进入新建页面</h2>
<p><img src="/img/bVM553?w=1800&h=1100" alt="新建页面" title="新建页面"></p>
<h2>2、选择合适的模板</h2>
<p>官方模板一共有4种,看官可以选择与自己实际业务相符合的模板。为了教程需要,笔者选择了 <code>nowa-template-uxcore</code> 模板进行演示。</p>
<p>官方模板旁边对应有 <em>Version</em> 字样,指的是模板里面使用的 UI 组件库的大版本。</p>
<p>如果官方模板太少,用户可以添加自定义的模板,以后会有专门的文章讲自定义模板。</p>
<p>让我们轻轻地点击 <em>create</em> 按钮进入下一步。</p>
<h2>3、填写表单信息</h2>
<p><img src="/img/bVM56j?w=1800&h=1100" alt="表单信息" title="表单信息"></p>
<p>每个模板都会含有一些的必要信息,比如 <em>Path</em> & <em>Npm Registry</em>,<em>Others</em> 是该模板的其他配置项,比如 <em>uxcore</em> 模板支持单页和多页应用与国际化选项。</p>
<p>如果用户的项目未来会包含私有源的私有模块的话,源地址最好填写私有源地址。</p>
<p>如果项目之前已经存在,会弹窗提示用户是否覆盖安装。</p>
<p>确认之后点击<em>提交</em>。</p>
<h2>4、静静等待依赖安装</h2>
<p><img src="/img/bVM560?w=1800&h=1100" alt="依赖安装" title="依赖安装"></p>
<p>安装结束之后,项目会自动导入到左侧的项目列表中。</p>
<p>接下来请看视频演示</p>
<p><img src="/img/bVM57k?w=894&h=539" alt="视频演示" title="视频演示"></p>
<p>完美!一个项目就这么快就创建成功了,不到一分钟的时间。</p>
<h2>5、运行项目</h2>
<p><img src="/img/bVNcAs?w=1800&h=1100" alt="图片描述" title="图片描述"></p>
<p>点击第一个大图标 '启动' 按钮后,按钮字样会变更为停止。这时候项目已经启动了,控制台的 '监听日志' 里面会看到对应的输出。</p>
<p>再次点击 '停止' 按钮,会结束对项目的监听。</p>
<p>用户不需要手动点开浏览器输入一大串的地址,工具会帮你做。用户只要点击 '访问' 按钮,工具会自动打开浏览器,并且链接到起始地址。</p>
<p><img src="/img/bVNcBf?w=2042&h=1152" alt="浏览器" title="浏览器"></p>
<p>一个简单的网页就出来了。</p>
<p>欢迎各位看官<a href="https://link.segmentfault.com/?enc=66paiNaMpvQiHA7vBFKUww%3D%3D.JaK0BIi6oIJmtXq6eVcM%2FT51%2B9WiCjlt4reCjd9ebsA%3D" rel="nofollow">下载</a>体验。</p>
<p>如果觉得不错,请到 <a href="https://link.segmentfault.com/?enc=hLABLscyvH8KK9L%2FNHaj6w%3D%3D.9Z0fYArEddmsonGv0MbhXGk6EQLH3AVP9BybNYIPb%2FBZ5JN87s%2FGqKK3Dk2hoPL9" rel="nofollow">github</a> 上面点星星。</p>
Nowa 上手篇(1)- 介绍
https://segmentfault.com/a/1190000009278983
2017-05-03T17:26:26+08:00
2017-05-03T17:26:26+08:00
Poling
https://segmentfault.com/u/poling
2
<p>这个专题主要是详细介绍 Nowa 工具,为此码字无数。</p>
<ul>
<li><p><a href="https://segmentfault.com/a/1190000009278983">Nowa 上手篇(1)- 介绍</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009279332">Nowa 上手篇(2)- 创建 React Web 项目</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009330713">Nowa 上手篇(3)- 工具使用指南</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000009661892">Nowa 上手篇(4)巧用命令集</a></p></li>
<li><p>...</p></li>
</ul>
<p><img src="/img/bVM50T?w=285&h=295" alt="nowa" title="nowa"></p>
<p>Nowa 的意思是 'Not Only for Web App, but also for anything',其实笔者第一次接触到它的时候,还以为是“诺娃”的意思,可能是纪念开发者的媳妇儿,开个小玩笑。</p>
<p>不想看废话的请直接拉到底部看总结。(^-^)</p>
<h2>初衷</h2>
<p>Nowa 面向的用户主要是前端新手用户。工具希望能节约用户的学习前端构建知识的时间,简化项目的构建,开发和调试流程,从而让用户专注于前端业务开发。</p>
<p>对于缺少 Node 和架构知识的新前端来说,学习框架已经占用较多的时间成本了,要再花精力去自己搭脚手架,那会让学习曲线变的更为陡峭。我们其实可以从中发现,搭建 Server 跑项目这些过程是可以与内在的前端框架剥离的。任框架18般轮着换,放兵器的架子可以不用变。<br>所以,Nowa 就是为了以不变应万变的需求诞生的。此处应有掌声。</p>
<p>Nowa 核心是基于 Webpack 进行二次开发的前端开发套件,后来再套上了一层 可视化的 Electron 壳子。现在,用户只需点几下按钮就能够创建一个 React 应用,不用为搭建 Webpack 生态,下载一坨坨组件而烦恼。</p>
<h2>小野心</h2>
<p>当然,Nowa 的立足点不仅仅是个构建工具这么简单,市面上这类型很多,它还希望是一个开发平台。为此,Nowa 提供了一种模板机制,拓宽了狭隘的工具视野。用户可以基于给定的规范提交前端框架模板,比如 React & Vue & Angular 模板等等。模板因为剥离了构建流程,很大程度上丰富了 Nowa 的使用场景,从而支持各种各样的项目。</p>
<p>说人话就是,我们有一个像橡皮泥一样的模板,任君蹂躏。</p>
<h2>小纪事</h2>
<p>Nowa 其实含有两个版本,ck 在 2016 年开发了命令行版本。然后鄙人加入该组,在 2017 年初开发可视化版本。命令行版本目前处于维护阶段,可视化版本还处于开发阶段,不过请放心拿去使用吧,俺不怕你们怼。</p>
<h2>总结</h2>
<ul><li><p>Nowa 是什么?</p></li></ul>
<p>Nowa 是基于 Webpack 封装的多平台构建工具,帮助用户省去构建步骤,能够快速可视化地搭建 React 应用。</p>
<ul><li><p>Nowa 有什么用?</p></li></ul>
<p>Nowa 省去了用户研究打包,开发,模拟数据等 N 项变态又无聊的工作。用户可以直接上手写业务代码,不用考虑如何压缩合并代码,如何热加载,如何代理资源等等,如何配置国际化功能等等。</p>
<p>能偷懒的,我们都帮你做了。</p>
<ul><li><p>Nowa 怎么用?</p></li></ul>
<p>请看接下来的<a href="https://segmentfault.com/a/1190000009279332">Nowa 上手篇(2)- 创建 React Web 项目</a>。</p>
<ul><li><p>Nowa 美颜</p></li></ul>
<p><img src="/img/bVNcCM?w=2024&h=1324" alt="图片描述" title="图片描述"><br><img src="/img/bVNdsw?w=2024&h=1324" alt="图片描述" title="图片描述"><br><img src="/img/bVNdsH?w=2024&h=1324" alt="![图片描述" title="![图片描述">]</p>
<ul><li><p>Nowa 官网</p></li></ul>
<p><a href="https://link.segmentfault.com/?enc=vQFA2HB%2B2xzlTnVYHly0Vw%3D%3D.hqD0RHSbEMa6V2fFP7tdpUDhy0JrSdD4iWp%2BrU7qleg%3D" rel="nofollow">戳这里</a>,我们提供了 Windows & Mac & Linux 三个平台的版本,不怕你找不到,就怕你不要。</p>
UXCore 组件单测的一些事儿
https://segmentfault.com/a/1190000008328277
2017-02-12T22:12:26+08:00
2017-02-12T22:12:26+08:00
穆心
https://segmentfault.com/u/fe_girl
3
<blockquote><p><a href="https://link.segmentfault.com/?enc=sXtU7j3OFPj6YQAgszVNRw%3D%3D.gbco8OLzFc9mnzGsDmypyg%3D%3D" rel="nofollow">UXCore</a> 是 XUX 团队开源的 PC 端 React UI 套件,作为一个支持企业级应用的 React UI 开源套件,为了保持项目的持续集成,良好的单元测试必不可少。本文来分享一下在编写单元测试的过程中遇到的一些问题和总结。</p></blockquote>
<p>在编写单测的时候首先可能会遇到一个开发顺序的问题,是先写测试代码还是先写程序代码,那么先来介绍两种开发技术:<a href="https://link.segmentfault.com/?enc=Q3kS6VqiCdwJitxorYhX%2Fw%3D%3D.O0lgdu6rIFc%2FrZh8uJe5Yio3iq9BmUCU4COpBUzHfxL8VW%2BXO0IXkId%2B%2BjtvlOdqcYjWs9tk%2BgMcbjDPZi%2F7iRosuN%2BxSDMbkMW2HZ8lHCeVgNZXpIv9mrN4LKRwv9No" rel="nofollow">BDD</a> 和 <a href="https://link.segmentfault.com/?enc=Kw6ONGXQchVbeL1Yp8KS2w%3D%3D.YcArwUCwSBNoTvHGMU5wC2FX%2B13J1E7D%2BoEnnwtWSRR4UxOxJRA8zn5iTuZcYqXzDqu59K151VU3y%2BThuX%2BLnV7%2B0EyESkuhUfuLWmv9ZPlJXq1j2ThDShfr7oull8dX" rel="nofollow">TDD</a>。BDD 是行为驱动开发,更趋向于需求,从写单测的顺序上可以理解为先了解行为,然后去写单测来检测是否符合需求期望;TDD 是测试驱动开发,先编写符合需求期望的测试,然后根据测试来实现最简功能代码,不断进行迭代打磨代码,在写单测的顺序上那就是先写单测,根据单测来编写功能代码。这两种开发技术分别适用于不同的场景,可根据自己的情况来选择使用。</p>
<p>UXCore 目前已有的组件采用的是 BDD ,由于组价代码都已 ready 我们就编写测试来验证需求是否已经满足,首先要做的就是通读组件代码(因为有的组件的作者不是自己),将其内部逻辑和实现要搞清楚,接下来才会知道怎么具体写测试代码!</p>
<h3>UXCore 组件单测环境</h3>
<p>在编写单测时我们要将测试代码运行起来,实时看到测试结果,并且要查看测试覆盖率以更好的提高代码质量,那么这就需要测试运行器、测试框架等等工具或框架来支持,以下是 UXCore 组件采用的单测环境,其他 React 项目也可参考使用。</p>
<ol>
<li><p>测试运行器(Test runner)主要负责测试的自动化,它可以自动调起多种浏览器或者 js 环境让你来调试测试代码。目前有 <a href="https://link.segmentfault.com/?enc=ZtofHsJBr0JZKkKbtHy8xw%3D%3D.LeB2AwsY47uEEvxf0A4BpE2AY6LvOf3itQnv%2BCyLucit33B86fUjpvDmB5E3VyhFeYo7k7i2GO7usluQXeJJdA%3D%3D" rel="nofollow">JSTD</a> 、<a href="https://link.segmentfault.com/?enc=wFswgj6WzqCGT5XW2tQwLQ%3D%3D.2n6VSVfs6xR0kVzObKs77Ugi5GyDMW7iGMpFlrmZJcICPuqp2Qu3dTmx%2BWcw%2BRpZ" rel="nofollow">Karma</a> 、<a href="https://link.segmentfault.com/?enc=yfHUFYnvBRqKGuoIMizTHA%3D%3D.C4nWRNMNOB6he9YcHG2e6VKo7ZFTGIIzvnBdgf9ydy8%3D" rel="nofollow">TestSwarm</a>、 <a href="https://link.segmentfault.com/?enc=xz1HHq03W12pto1A3OaxPg%3D%3D.VAZEcBugJl5pI4iLw3%2BPtmfiN9R7q15ozS%2B%2Fxs8xaQlElHLQqLyzUM9O7gXZjyXu" rel="nofollow">Buster</a> 等测试运行器,UXCore 使用的是 <a href="https://link.segmentfault.com/?enc=0%2FAsmmBPjy9GTpOOFa7iDA%3D%3D.YkA5LzLN9lfJEDZ6Ottm%2Bx16Xx5fbSu6Ow3KgdVopJ%2FedGPBY%2BUH1EOCWa5tKQjr" rel="nofollow">Karma</a> ,原因是它集成了 Istanbul 的 coverage 功能,可检测测试覆盖率,并且支持多种测试框架。在 uxcore-tools 中已经集成 karma ,你只需执行几个命令就可以。</p></li>
<li><p>测试框架(Testing Framework),即运行测试的工具,测试框架规定了测试风格和测试的生命周期,通过测试框架为程序添加测试,保证代码质量。目前有 <a href="https://link.segmentfault.com/?enc=fhM04btJwJONvXdDqaXs1Q%3D%3D.uppOpLymDNViyzaxvRpcelezN7FIT%2F93w27Rl%2FNTBlg%3D" rel="nofollow">Jasmine</a> 、<a href="https://link.segmentfault.com/?enc=TgyG1jiUA%2Bq%2FqgsNf7w18Q%3D%3D.oBilJ15obciV3HwAGc%2FqTMMM3s0KopRM5BxQBmG6LW4%3D" rel="nofollow">Qunit</a> 、<a href="https://link.segmentfault.com/?enc=YW0p4UzG22gyO1inkAAlDA%3D%3D.3Q0G8N4R8Fp9RWAXLk19p9wdSR5q9nf1249hRE8Hu6I%3D" rel="nofollow">Sinon</a> 、<a href="https://link.segmentfault.com/?enc=IHowGin4GN1zYMgFlUxojw%3D%3D.0obwuszyT44bhQZ32XE3Jqh6sm02Mh5rE0pkc%2BUIYkM%3D" rel="nofollow">Mocha</a> 等测试框架。UXCore 选择的是 <a href="https://link.segmentfault.com/?enc=Ud0xeJI3jwaxobLcH8mN8w%3D%3D.3ZVcBPeuQgkMbab%2B%2FaLlgE0Bq1K4bIFXZUD6lPep%2BOQ%3D" rel="nofollow">Mocha</a> ,它来自 TJ 大神,不但可以在bash中进行测试,而且还拥有一整套命令对测试进行操作,Mocha 的异步测试做的比较好,在 it 的回调函数中会获取一个参数 done ,类型是 function ,用于异步回调,当执行这个函数时就会继续测试。</p></li>
<li><p>当然,除了测试运行器 、测试框架之外我们还需要断言库(Assertion library)来帮助我们判断代码执行的正误,比较流行的断言库有:<a href="https://link.segmentfault.com/?enc=NmI2wcaCnIgLSxXcfpD68Q%3D%3D.Qi0RE4vGZljq5cqHZ1o0%2B5W2Ewy2Tg42RXKihPfr7ll9PB9hwadgc3WOKlJK8IYr" rel="nofollow">expect.js</a>、<a href="https://link.segmentfault.com/?enc=bGcBXIJRGBzJvajwRPw9wg%3D%3D.AOdNnpb2V4XYOMIyYiiKMZzpedZMVRuo87zpQXCu%2Ffm1eoOPA%2FnFE2NKhc2%2FIsLc" rel="nofollow">should</a>、<a href="https://link.segmentfault.com/?enc=Jvk0R%2BWwT60PaFdsJqMfng%3D%3D.YuFkI%2BxsSK9mwKD2%2F3pyHG2UTR2x6PcGJWRbwjh0rYBceqR8bzbpKIL6K844VkuX" rel="nofollow">chai</a>,我们使用的是 expect.js ,支持 IE6+ ,语法通俗易懂。</p></li>
<li><p>在测试 react 组件时还要用到 React UI 测试库,<a href="https://link.segmentfault.com/?enc=NO%2FF7Q12u9yMeleAvKikpg%3D%3D.0DNfdLxxxupb0IXX7z6rFt8EXgkR51dzuwOQAUTUKpbqoVa%2BAattOenw0cVN9TUkv4BW9qHEe9g3Wu4tp2J%2FgA%3D%3D" rel="nofollow">React 官方提供了工具库</a>,它对虚拟 DOM 和 真实 DOM 都有对应的测试方法,但是它使用起来不太方便。还有一个 Airbnb 公司的封装的 <a href="https://link.segmentfault.com/?enc=TOhiUoO4gDY9vcd8ceqdvw%3D%3D.KKiXbrvfVysuuIEd0KG1ngNfDByajF71p4SLPfeaYU%2FXY37FMro3mCOsA5sCwCDg" rel="nofollow">Enzyme</a> ,非常容易上手。我们选择的是后者。</p></li>
</ol>
<h3>测试覆盖率</h3>
<p>测试覆盖率是测试中的一种度量,描述源代码被测试的比例和程度。通过测试覆盖率可以检测哪些代码没有被测试,但是测试覆盖率并不能作为衡量代码质量的标准和目标,而是一种发现未被测试代码的手段。</p>
<p>覆盖率的统计的核心思想就是,在源代码对应的位置注入统计代码,当代码运行后,根据统计代码统计的数据确定程序运行的路径,最终生成覆盖率统计报告。整个过程可以分为三部分:转换(instrument)、执行(run)、生成报告(report)。</p>
<ol><li>
<p>转换(instrument)</p>
<ul>
<li><p>首先对源代码进行语法分析生成语法树</p></li>
<li><p>在语法树相应的位置注入统计代码,在程序执行到这个位置的时候对相应的全局变量赋值,确保执行之后能够根据全局变量知道代码的执行流程</p></li>
<li><p>根据注入之后的语法树生成对应的 JavaScript 代码,即转换之后的代码(instrumented code)</p></li>
</ul>
</li></ol>
<blockquote><p>注:这里进行语法分析的好处是,针对书写不规范的代码(比如一行多个语句),依然能够很好统计出分支覆盖和组合覆盖等信息。反过来想,如果想要提高覆盖率,那么代码要书写规范。</p></blockquote>
<ol><li><p>执行(run)</p></li></ol>
<p>这一步需要先载入转换后的代码:</p>
<ul><li><p>浏览器环境:需要将转换后的代码传给浏览器。如果是 karma 之类的带 server 的测试框架,需要通过 socket 传输至浏览器,执行完之后再将包含覆盖率信息的执行结果传回 server ,生成测试报告。</p></li></ul>
<p>然后执行单元测试,产生的统计信息会挂在全局变量 this 下面。对于浏览器环境,this 就是 window。</p>
<ol><li><p>生成报告(report)</p></li></ol>
<p>这一步会根据全局标量中的覆盖率信息生成特定格式的报告,如 html、lcov、cobertura、teamcity等。</p>
<p>想要了解更详细的内容可移步:<a href="https://link.segmentfault.com/?enc=UDl89QUoMe7SwGS35YWzpQ%3D%3D.kg1EdoU90Hl96oFJcJuDPtlERLvJboV2o0rvSQZv9ap3LQUB0IQKHdmMymXkhtip" rel="nofollow">https://yq.aliyun.com/article...</a>,如果想了解 <a href="https://link.segmentfault.com/?enc=m7ddvfbRP9ZAeQhGYYdKkQ%3D%3D.XxIgh22zDH0vklTyPJ8lx059qBoUKLAszXzgFRbCgXeV93ew38k%2FaHT6zlP1hAZ7IW27o8JxWl29laQ7oJH2dQ%3D%3D" rel="nofollow">Istanbul入门使用</a>的童鞋可以看一下阮大神的这篇。</p>
<h3>如何编写 React 组件单测</h3>
<p>编写单元测试就是用一段代码来测试一个模块或功能是否能达到预期结果的一个过程。</p>
<p>下边列出两个常见的例子给大家作为参考:</p>
<ol><li>
<p>最简单的 props 测试。</p>
<ul><li><p>首先创建组件的真实 DOM 结构</p></li></ul>
<pre><code class="javascript">const wrapper = mount(<Select2 prefixCls="kuma-select2" ></Select2>);</code></pre>
<ul><li><p>然后获取到当前组件的 prefixCls props,用 enzyme 的 <a href="https://link.segmentfault.com/?enc=Ho6T4D2X0%2Fd6PyLxDCz3%2FQ%3D%3D.ZRadPMLf0BwMXLV0AybN7MBvXCLo1isjkozADL7GqNuokd7BlBPFvQ5JlizkUA77m9fIs1dz0baXJHQ5JbecgAUUILEC6KWYkQgci2YplWk%3D" rel="nofollow">.props()</a> API 就可以很容易的取到 props 的值(这里有一点要说明,.props() 只能取到 root component 的 props),然后用断言句进行判断当前的 prefixCls 是否是 'kuma-select2'</p></li></ul>
<pre><code class="javascript">expect(wrapper.props().prefixCls).to.equal('kuma-select2');</code></pre>
<ul><li><p>完成上边两步就可以了吗?上边只是 get props 符合了我们的期望,那我们再给组件更新一下 props 看会不会生效。enzyme 也提供了对应的 <a href="https://link.segmentfault.com/?enc=t%2F0OIOhRMEh%2BoE%2BY0Y8oPA%3D%3D.8OK4s2qBxY171%2FVmG%2BNTCEWsMSHq%2FQ88G0%2F%2F3Iy%2BeTQvj3fuO3YQ2Di4d8rScOCdZyw3j3PdI8SQhFiieeP4DaSNKHiKRP32hO7XwWd322o%3D" rel="nofollow">.setProps()</a> 的 API ,真是太方便啦。然后再次验证新的 prefixCls 有没有更新。</p></li></ul>
<pre><code class="javascript">wrapper.setProps({ prefixCls: 'kuma-select2-test' });
expect(wrapper.props().prefixCls).to.equal('kuma-select2-test');</code></pre>
</li></ol>
<ol><li>
<p>模拟下拉框选中动作,测试 value 值是否已更新。</p>
<ul><li><p>首先还是创建组件的真实 DOM 结构</p></li></ul>
<pre><code class="javascript">const wrapper = mount(<CascadeSelect options={options} />);</code></pre>
<ul><li><p>然后要创建 Trigger 组件( dropdown的子组件 )的真实 DOM 结构,这里要提一下 enzyme 的 <a href="https://link.segmentfault.com/?enc=XiNhESaveDUZdHws8uKNwA%3D%3D.TDGcM%2F080RmOpLGv5MZKp3U8btZQC5EcBEknerqHHNTzpMai5UpoWYBDG6WZO3N8GY9VSr40ISpPQRPxQxaKDQ8IDmjO2AQGHwpff%2Fun%2FS0%3D" rel="nofollow">.find()</a> API ,它的参数除了可以是选择器外还可以是子组件的名称。</p></li></ul>
<pre><code class="javascript">const dropdownWrapper = mount(wrapper.find('Trigger').node.getComponent());</code></pre>
<ul><li><p>接下来找到下拉选项中的第一项触发 click 事件,<a href="https://link.segmentfault.com/?enc=MkgiwN8Hw63k39aZlViAvw%3D%3D.ZH6Dbe1y0uFSePf7uwzEhvUKzlJXWikCKukCwBYuy3KjNKWae0UnzJL48KacvGV0Q28r%2BOQv1VzM1oJcwRUbkzn0MhXkO36BWr%2Fjg9Csuz4%3D" rel="nofollow">.simulate()</a> API 用来做事件绑定。</p></li></ul>
<pre><code class="javascript">dropdownWrapper.find('li').at(0).simulate('click');</code></pre>
<ul><li><p>模拟完后就来验证你要验证的逻辑,获取到子组件 CascadeSubmenu 的一项 option ,调用 CascadeSubmenu 的 props onItemClick 函数,将获取到的 option 传入进去,最后断言句判断最后组件的 value 是否不为空。</p></li></ul>
<pre><code class="javascript">const option = dropdownWrapper.find('CascadeSubmenu').props().options[0];
dropdownWrapper.find('CascadeSubmenu').props().onItemClick(option,2,false);
expect(wrapper.state('value').length > 0).to.be.ok()</code></pre>
</li></ol>
<p>上边的代码使用了 Enzyme 的 <a href="https://link.segmentfault.com/?enc=6DXT0nqE3j6L%2FIzMowzKWw%3D%3D.5WUXWZ3vUUN%2FQfsmu1Cf98E4fpRSRXd9x4FsH%2BV35EUS5bxeI7LR1ArJggon%2BjQIwfReqMuLuh%2Fdzc5iuRF8fg%3D%3D" rel="nofollow">Full DOM Rendering API</a> ,首先创建 CascadeSelect 组件的真实 DOM 结构,然后再次创建 dropdown 的 Trigger 子组件的真实 DOM 结构,最后找到第一个选项触发 click 事件。</p>
<blockquote><p>Tip:如果组件引用了其他的组件,那么单测应该测到什么程度呢?要不要测引用的其他组件的代码?答案就是:你不需要测试它引用的组件的代码逻辑,引用组件的代码测试会在它自己内部完成,当然你测了也不会提高覆盖率,我们只需测试我们组件的代码即可。</p></blockquote>
<p>最后,怎么能更加清晰的知道自己哪些代码还没有覆盖到呢?如果可以看到就好了,能够对症下药,绝对提高覆盖率啊!当然,Karma 都帮你做好了,执行过 coverage 后会生成一个 coverage 目录,它的 src 目录下的组件文件命名的 html 文件打开后就会看到非常清晰的记录,红色的表示还没有覆盖到的。当然也可以本地起一个服务,可用 anywhere 直接浏览器里查看覆盖率列表和对应的详细文件。下图是一份coverage 报告。<br><img src="/img/remote/1460000008328280?w=1018&h=407" alt="" title=""><br><img src="/img/remote/1460000008328281?w=472&h=281" alt="" title=""></p>
Icon 进化史
https://segmentfault.com/a/1190000008199111
2017-01-23T10:22:24+08:00
2017-01-23T10:22:24+08:00
matianming
https://segmentfault.com/u/maming
8
<h2>“南方古猿”之 png sprite</h2>
<p><img src="/img/remote/1460000008199114?w=822&h=288" alt="" title=""></p>
<p>看到上面这张图,相信好多资深前端会感到很亲切。</p>
<p>早期为了减少资源的请求,人们想到了将小的 png 图片合并到一张图上,然后根据 <code>background-position</code> 来显示不同的图片。</p>
<p>早期还有靠人肉来测量坐标,随着构建工具的发展,我们可以用一些插件,如 <a href="https://link.segmentfault.com/?enc=%2B70%2F%2BS1QLkx%2FVH7KXryzIw%3D%3D.Vm8%2FSz46JwJwVaxrzvzwKA%2BgNClsmU1ZGXT6diJ%2FK9MUvsH6wd3n7pFGtFeHXJu1" rel="nofollow">grunt-spritesmith</a>、<a href="https://link.segmentfault.com/?enc=%2B5E7FXge1GW5nW3Tutcdvw%3D%3D.qlTH0ibNGDxdAuttuC0QHoNWZ6GYs4BzwsZL7P%2BsMm3vCQZKhsTDFRUMyls5Oq0V" rel="nofollow">gulp.spritesmith</a> 等。它可以帮助我们自动合成,并生成好 css, 位置都计算好的。</p>
<p>那么使用 png 图片这种方式它的优点就是兼容性好。但是一旦开发多了,它的不便变体现出来了,换颜色、改大小、透明度、多倍屏等等。</p>
<p>所以对于这种方式我们只能缅怀。</p>
<h2>“能人”之 Iconfont</h2>
<p>于是人们又想出了用字体文件取代图片文件:Iconfont。</p>
<p>虽然早期制作或寻找合适字体比较麻烦,但随着各种字体库的网站出现,如: <a href="https://link.segmentfault.com/?enc=sCeT5VvAaxGrQ4KESmDpVQ%3D%3D.rtCD6pzIBLrAvHvBH3IcnHp6XHpKzjBJsRW53m5eq68%3D" rel="nofollow">http://www.iconfont.cn</a> ,那都不是事了。再加上 css 的自定义字体的兼容性非常好,Iconfont 迅速开始流行起来。以这个站为例,大概看下使用方法:</p>
<ol>
<li><p>在 Iconfont 中创建自己的项目,将需要使用的图标添加到自己的项目中。</p></li>
<li><p>复制出 Unicode 或 Font class</p></li>
<li><p>全部代码如下</p></li>
</ol>
<pre><code class="css">@font-face {
font-family: 'iconfont'; /* project id 38792 */
src: url('//at.alicdn.com/t/font_1444792316_9706304.eot');
src: url('//at.alicdn.com/t/font_1444792316_9706304.eot?#iefix') format('embedded-opentype'),
url('//at.alicdn.com/t/font_1444792316_9706304.woff') format('woff'),
url('//at.alicdn.com/t/font_1444792316_9706304.ttf') format('truetype'),
url('//at.alicdn.com/t/font_1444792316_9706304.svg#iconfont') format('svg');
}</code></pre>
<pre><code>.iconfont {
font-family:"iconfont" !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-tishi:before { content: "\e600"; }</code></pre>
<pre><code class="html"><i class="iconfont icon-tishi"></i></code></pre>
<p><a href="https://codepen.io/maming/pen/YNpMwd">这里有demo</a></p>
<p>在实际开发中,我们会把常用的一些图标封装成<a href="https://link.segmentfault.com/?enc=9wcs%2BejVWzk1lmi2VYAwNA%3D%3D.sC1LJUHzL0qFXbio%2FX32Q%2FgLcRtCbuTD51pbUyIfxd%2F4XkBw4ovP3bVbODmRouaI" rel="nofollow">组件</a>,直接使用。像这样</p>
<pre><code class="html"><i class="kuma-icon kuma-icon-success"></i></code></pre>
<p>Iconfont 用起来挺方便的,而且兼容性也十分的好,大小、颜色可随意改变。</p>
<p>但它仍有缺陷:</p>
<ol>
<li><p>字体需要加载资源</p></li>
<li><p>有时候可能会出现锯齿</p></li>
<li><p>只能被渲染成单色或者css3的渐变色</p></li>
</ol>
<p>所以我们要继续进化。</p>
<h2>“直立人”之 svg symbol</h2>
<p>使用 svg ,这里所谓的进化并不是 svg 本身的进化,因为 svg 并不晚于 Iconfont。只是环境(兼容性)的原因导致它无用武之地。就像最近火的一塌糊涂的 AI, 其实最早在 1956 年就提出了。随着外界因素的进化,IE6/7/8 的淘汰, android 4.x 的开始,svg 的机会变到来了。先看下兼容性:<br><img src="/img/remote/1460000008199115?w=2492&h=724" alt="image" title="image"></p>
<h3>
<a href="https://link.segmentfault.com/?enc=0OopzRtbLvioXaMraydTsQ%3D%3D.yP4gRu6uKyf1Ylj%2Bh0%2FWJcmUtVjX4ZMyY%2FXA3LHiOjyKk02HTTbDBhHDUufcbgpwkbcKExoamU%2BNqTwE5KYFhQ%3D%3D" rel="nofollow">svg</a> 的使用方式:</h3>
<ul>
<li>
<p>保存成文件</p>
<ul><li><p>需要请求加载资源</p></li></ul>
</li>
<li>
<p>inline 方式</p>
<ul><li><p>在 html 一坨坨,很麻烦</p></li></ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=2AV758tEmCnQLGUQCAjEsA%3D%3D.YDtg0DfEVA%2FIzLZsj5tLoB6URFcnuaAE5MQghk%2BDEd2eZjnLH99i5COi3a6BhlIOuSgAkHdOB%2FeXX9nkN8xImw%3D%3D" rel="nofollow">symbol</a></p>
<ul><li><p>适合我们做组件</p></li></ul>
</li>
</ul>
<blockquote><p>The <code><symbol></code> element is used to define graphical template objects which can be instantiated by a <code><use></code> element.</p></blockquote>
<p>通过 <symbol> 定义的 svg 模板,我们可以使用 <use> 来加载它。</p>
<pre><code class="html"><svg width="120" height="140">
<!-- symbol definition NEVER draw -->
<symbol id="sym01" viewBox="0 0 150 110">
<circle cx="50" cy="50" r="40" stroke-width="8"
stroke="red" fill="red"/>
<circle cx="90" cy="60" r="40" stroke-width="8"
stroke="green" fill="white"/>
</symbol>
<!-- actual drawing by "use" element -->
<use xlink:href="#sym01"
x="0" y="0" width="100" height="50"/>
</svg></code></pre>
<p>那么 <symbol> 是怎么来的呢?</p>
<p>同样,在这个构建工具十分发达的时刻。<br>最开始我们使用了 <a href="https://link.segmentfault.com/?enc=Z0PAxY27zp4HpdxKYQhm4Q%3D%3D.k1BQRYJ0m%2F3bHlL6PfEWrTFyvdBNjHvbTLuZc44FsTVGHo1wH1phbbiLv92lSs8E" rel="nofollow">gulp-svg-symbols</a>,它可以将指定目录中的 svg 自动合并到一个 svg 文件中,文件里包括了所有 icon 的 symbol 模板,然后再将这个模板将其隐藏放到 index.html 中。</p>
<p>但是这个库有个坑点是它依赖了一个 Unicode 的包,这个包在国内安装炒鸡慢,于是我们弃用了它,使用了<a href="https://link.segmentfault.com/?enc=7um1jigysUXoYX0PL0TChw%3D%3D.9oMOdDoQoFAN283VLF4w9LuBMSJDUgVEIVj3O1TllQTGqGTQQhFvnzxXo1OsD0Xd" rel="nofollow">gulp-svgstore</a></p>
<p>按照这种方式我们成功的开发一 <a href="https://link.segmentfault.com/?enc=bgW%2F6Uaeb6hQ8PjarFWItw%3D%3D.7RoU1x%2B7z2CWyj4fictdufWXT7WdMIdJR3twZ3HhUzJK6wlMqbT%2Bj5HGEpTAppT3Ygo6QN0HZFVBC%2F4bYH5sfJUiUwwLuv7rnynD%2FI12mWI%3D" rel="nofollow">salt-icon</a> 这个组件,里面包括了一些常用的 icon。使用方式:</p>
<pre><code class="javascript"> <Icon name="success" className="icon-success"/></code></pre>
<p>这样我们在 mobile 端用 svg 替代了 Iconfont,解决了上述 Iconfont 提到的问题。</p>
<p>但是很快我们就发现,在 index.html 中引入那一坨 symbol 模板是极其恶心的。</p>
<p>随着 <a href="https://link.segmentfault.com/?enc=S6lMAjL1P2g4ctJEPNi3zw%3D%3D.SUMkeQNgrUInatoAGN4IwQKoXHDbdikXV0eXiS9V5Dw%3D" rel="nofollow">webpack</a> 打包的成熟,各种 loader,我们将那一坨 symbol 模板直接打包成一个 salt-icon-source.js 文件,在这个文件中将其 append 到 body 上。</p>
<p>同时发现了上述提到的 <a href="https://link.segmentfault.com/?enc=dq0JE%2BXoYcEmXGbQgjNe5Q%3D%3D.QCV8ZUVbxTzSTRD6ZAfKX9a3eGqRxr36CQFI2uePG6g%3D" rel="nofollow">iconfont 网站</a>也支持直接导出 symbol 文件。</p>
<pre><code class="javascript">import React from 'react';
import ReactDOM from 'react-dom';
import IconSource from './svg/salt-icon-symbols.svg';
const WRAPPER_ID = '__SaltIconSymbols__';
const doc = document;
let wrapper = doc.getElementById(WRAPPER_ID);
if (!wrapper) {
wrapper = doc.createElement('div');
wrapper.id = WRAPPER_ID;
wrapper.style.display = 'none';
doc.body.appendChild(wrapper);
ReactDOM.render(<IconSource />, wrapper);
}</code></pre>
<p>这样虽然解决了引入模板的那个问题,但是后面又发现的 symbol 在安卓 4.3.x 下无法显示,也就是说 symbol 的兼容性并没有直接使用 svg 好。</p>
<p>然后我们通过使用一个叫 <a href="https://link.segmentfault.com/?enc=16%2FkRf5vFrofBfvOWhVtMg%3D%3D.tP0g5R6u%2Bg%2BqIgNjW%2B0qffY6UPmaE7ZM22j9C0mUNyPv2JbE%2FETTaWaVkADnFX3I" rel="nofollow">svg4everybody</a> 的库,解决了上述兼容性问题。(它的原理是如果发现不支持 symbol 的,它会通过 xlink:href 拿到 svg 的资源,然后动态创建一个 svg,插入到当前位置)</p>
<p>虽然解决了兼容性的问题,但是我们深深的感觉到了这种方式的不优雅。</p>
<p>讲的这里,可能会有人会有疑问,既然已经有 <a href="https://link.segmentfault.com/?enc=V0UVWgGWVQFIMxYJHXo0Fw%3D%3D.U97J9LFdS85XsAY7uuHmeMoCTk0pDTAnhJ9CiAXdbUZfSXkjNoJvPb%2Bux81YA9zW" rel="nofollow">svg-react-loader</a> 了,为什么不直接 import svg 文件?</p>
<p>业务中使用的图片当然可以直接 import 加载,但一些通过的图标我们希望是能统一起来,做出组件,更方便的使用。</p>
<p>而且我们组件中还会对 svg 处理了它事件不能冒泡的问题,也就是在某些低版本的安卓机上,svg 图标是无法点击的。解决方案有两种:</p>
<ul>
<li><p>贴膜,不过这样会导致多一层结构嵌套</p></li>
<li><p>去掉事件</p></li>
</ul>
<pre><code class="css">svg {
pointer-events: none;
}</code></pre>
<p>不过,这个问题可以给我带来启示,‘既然已经有 <a href="https://link.segmentfault.com/?enc=tudAGRfPUcOI%2FzeBBUUx3g%3D%3D.8IFKw6F7vPxz%2F0S%2B5%2Bb3a9QHxxelowr5XCsvmL0irlOErDvAokL3ugBhpmxlgvTh" rel="nofollow">svg-react-loader</a> 了’,那么 svg-loader 里做了什么呢?symbol 的方式或许真的可以淘汰了。</p>
<h2>“智人”之 svg</h2>
<p>看下 <a href="https://link.segmentfault.com/?enc=wqZoq6vEHUWtFxzsQ39sSg%3D%3D.ZGRngvnKzCHNm%2F77lo9zgP8LwScOIj6SGf1Zx5%2B5GSnTVFXnKSYBbu%2Bb8CVX94rG" rel="nofollow">svg-react-loader</a> 的实现<br>首先有一个模板</p>
<pre><code> render () {
const props = this.props;
return (
<svg {...props}>
<%= innerXml %>
</svg>
);
}</code></pre>
<p>然后有一个 sanitize.js ,会对 svg 做一些处理,加上标准的 xml namespace, 把 React 特有的属性 class / for 转化为 className / htmlFor, 把属性名转化为驼峰。</p>
<p>最后根据模板生成这样一段代码</p>
<pre><code class="javascript">var React = require('react');
module.exports = React.createClass({
displayName: "Test",
getDefaultProps () {
return {"width":"1024","height":"1024","viewBox":"0 0 1024 1024","version":"1.1","xmlns":"http://www.w3.org/2000/svg"};
},
render () {
var props = this.props;
return <svg {...props}>
<path d="M512.002047... fill="#272636"/>
</svg>;
}
});"</code></pre>
<p>这样的代码我们就可以直接在 react 中直接使用了。</p>
<p>所以我们的组件借助这样的思想,完全弃用了 symbol 模式。</p>
<p>我们先扫描对应的 svg 文件,将其按上面的思路生成一个个单独的 js 文件。<br>在组件层面可以再封装一层,统一引入,提供一个通用的调用方式,和上面一样。</p>
<pre><code class="javascript"> <Icon name="success" className="icon-success"/></code></pre>
<p>更好的是你也可以单独引用每一个文件,减小使用体积。</p>
<p>最后我们憧憬一下,随着 react 15.x 的发布,react 对 svg 的支持越来越好了,随着 IE 8 也即将被遗弃,我们的 PC 端也有望从 Iconfont 转换到 svg 了。</p>
ES6 + Webpack + React + Babel 如何在低版本浏览器上愉快的玩耍(下)
https://segmentfault.com/a/1190000006930013
2016-09-18T14:59:22+08:00
2016-09-18T14:59:22+08:00
matianming
https://segmentfault.com/u/maming
26
<h2>回顾</h2>
<p>起因:</p>
<blockquote><p>某天,某测试说:“这个页面在 IE8 下白屏,9也白。。”<br>某前端开发: 吭哧吭哧。。。一上午的时间就过去了,搞定了。<br>第二天,某测试说:“IE 又白了。。”<br>某前端开发: 嘿咻嘿咻。。。谁用的 <code>Object.assign</code>,出来我保证削不屎你。</p></blockquote>
<p>在<a href="https://segmentfault.com/a/1190000006929961">上篇</a>,我们主要抛出了两个问题,并给出了第一个问题的解决方案。</p>
<ol>
<li><p><code>SCRIPT5007: 无法获取属性 xxx 的值,对象为 null 或未定义</code>,这种情况一般是组件继承后,无法继承到在构造函数里定义的属性或方法,同样类属性或方法也同样无法继承</p></li>
<li><p><code>SCRIPT438: 对象不支持 xxx 属性或方法</code>,这种情况一般是使用了 es6、es7 的高级语法,<code>Object.assign</code> <code>Object.values</code> 等,这种情况在移动端的一些 ‘神机’ 也一样会挂。</p></li>
</ol>
<p>本篇将给出第二个问题的解决方案, 并对第一个问题的解决方案有了更新的进展。</p>
<p>文章略长,请耐心看~嘿嘿嘿~</p>
<p><img src="/img/remote/1460000006930016?w=435&h=309" alt="image" title="image"></p>
<h2>正文开始</h2>
<p>想要不支持该方法的浏览器支持,无非两种办法</p>
<ol>
<li><p>局部引用,引入一个相同的方法代替,其缺点则是使用起来比较麻烦,每个用到的文件都要去引入。</p></li>
<li><p>全局实现,与之相反的方法是使用 polyfill ,其优点便是使用方便,缺点则是会全局污染,特别是实例方法,涉及到修改其 prototype ,不是你的类,你去修改它原型是不推荐的。</p></li>
</ol>
<p>针对这两种办法,提供出以下几种方案,供大家参考</p>
<h3>方案一:引入额外的库</h3>
<p>拿最常用的 assign 来说,可以这样</p>
<pre><code class="javascript">import assign from 'object-assign';
assign({}, {});</code></pre>
<p>其实这种也是我们之前的使用方式,缺点就是需要去找到对应的库,比如 Promise 我们可以使用 <a href="https://link.segmentfault.com/?enc=f4K87iZU79GcYnxI3cad3g%3D%3D.U%2B7WL0r1EG5BCwgvG1UmJsXkB3vI35OVO4bthBsIAKNYbsVNp8r4hoKgsHNQ6Ogb" rel="nofollow">lie</a> </p>
<p>另一方面一旦有人没有按照这个规则,而直接使用了 <code>Object.assign</code>,那这个人就可能被削。</p>
<h3>方案二:全局引入 babel-polyfill</h3>
<p>在项目的程序入口</p>
<pre><code class="javascript">import 'babel-polyfill';</code></pre>
<p>babel 提供了这个 polyfill,有了它,你就可以尽情使用高级方法,包括 <code>Object.values</code> <code>[].includes</code> <code>Set</code> <code>generator</code> <code>Promise</code> 等等。其底层依赖的是 <a href="https://link.segmentfault.com/?enc=%2B6ub6l1U6XJ2%2B8UUsD%2Bf7w%3D%3D.4NS5%2B8CwXZgHbvGJCoUyQmCxWXQOqrpsRBfg7FPhn49kYSMVDBKs6Sc4PDn6qVxz" rel="nofollow">core-js</a> 。</p>
<p>但是这种方案显然有些暴力, polyfill 构建并 uglify 后的大小为 98k,gzip 后为32.6k,32k 对与移动端还是有点大的。</p>
<p>性能与使用是否方便自己权衡,比如离线包后或也可以接受。</p>
<h3>方案三:手动引入 <a href="https://link.segmentfault.com/?enc=DBwfRDGqVo2h50MfkzcF1w%3D%3D.pYtjsOdYdVOvErKBHnUsSUQLcEjxbrlMHq5Gi7iIH7yZvzojUegutwSO8apgSZcV" rel="nofollow">core-js</a>
</h3>
<p>这个方案也稍微有些麻烦, core-js 里实现了大部分 e6、es7 的高级语法,具体列表可以去这里查看 <a href="https://link.segmentfault.com/?enc=BB00I5%2Fb30vVtqVkFjPGdA%3D%3D.abQdzJhm62vdSFE8P0CMs4Qwj9bKMT4ejFPoHtOh1Uzls8IgolEiMl0lttlD92Amw7gn7kA%2Be%2FoI5G6Ca3Aw0fWbk4AjQXxB69LbhKzM%2ByDIHvMVk0Az2ZR9IlwKNDG2AJcQeJU9xay3a10KXG9LpA%3D%3D" rel="nofollow">https://github.com/babel/babe...</a></p>
<p>我先截取一部分做下参考</p>
<pre><code class="javascript"> Object: {
assign: "object/assign",
create: "object/create",
defineProperties: "object/define-properties",
defineProperty: "object/define-property",
entries: "object/entries",
freeze: "object/freeze",
...
}</code></pre>
<p>具体怎么使用呢?找到要使用的方法的值,如:assign 是 "object/assign",将其拼接至一个固定路径。</p>
<pre><code class="javascript">import assign from 'core-js/library/fn/object/assign'</code></pre>
<p>或</p>
<pre><code class="javascript">import 'core-js/fn/object/assign'</code></pre>
<p>这里包含上述所说的局部使用和全局实现的两种</p>
<p>直接引入 'core-js/fn/' 下的即为全局实现,你可以在程序入口引入你想使用的,这样相对于方案二避免了多余的库的引入</p>
<p>引入 'core-js/library/fn/' 下的即为局部使用,和方案一一样,只是省去了自己去寻找类库。</p>
<p>但是,实际使用,import 要写辣么长的路径,还是感觉有些麻烦。</p>
<h3>方案四:使用 <a href="https://link.segmentfault.com/?enc=oSfs%2BWbEjo%2BToCJ7UMcHnA%3D%3D.k8Q27U%2FdpKGMU6M8st7hcR4bsd2C8L5nhClngJ2fNZ9YGc5uT3gC6nqDNqGkNx%2BdB2rpl6%2F%2BLP68n08Qq%2BHq4w%3D%3D" rel="nofollow">babel-plugin-transform-runtime</a>
</h3>
<p>本文会重点介绍下这个插件</p>
<p>先看下如何使用</p>
<pre><code class="javascript">// without options
{
"plugins": ["transform-runtime"]
}
// with options
{
"plugins": [
["transform-runtime", {
"helpers": false, // defaults to true; v6.12.0 (2016-07-27) 新增;
"polyfill": true, // defaults to true
"regenerator": true, // defaults to true
// v6.15.0 (2016-08-31) 新增
// defaults to "babel-runtime"
// 可以这样配置
// moduleName: path.dirname(require.resolve('babel-runtime/package'))
"moduleName": "babel-runtime"
}]
]
}
</code></pre>
<p>该插件会做三件事情</p>
<blockquote>
<p>The runtime transformer plugin does three things:</p>
<ul>
<li><p>Automatically requires babel-runtime/regenerator when you use generators/async functions.</p></li>
<li><p>Automatically requires babel-runtime/core-js and maps ES6 static methods (Object.assign) and built-ins (Promise).</p></li>
<li><p>Removes the inline babel helpers and uses the module babel-runtime/helpers instead.</p></li>
</ul>
</blockquote>
<ul>
<li>
<p>第一件,如果你想使用 generator , 有两个办法,一个就是引入 bable-polyfill 这个大家伙儿,另一个就是使用这个插件,否则你会看到这个错误</p>
<pre><code>Uncaught ReferenceError: regeneratorRuntime is not defined</code></pre>
</li>
<li><p>第二件,就是能帮助我们解决一些高级语法的问题,它会在构建时帮你自动引入,用到什么引什么。</p></li>
</ul>
<p>但是它的缺陷是它只能帮我们引入静态方法和一些内建模块,如 <code>Object.assign</code> <code>Promise</code> 等。实例方法是不会做转换的,如 <code>"foobar".includes("foo")</code> ,官方提示在这里:</p>
<blockquote><p>NOTE: Instance methods such as "foobar".includes("foo") will not work since that would require modification of existing builtins (Use babel-polyfill for that).</p></blockquote>
<p>翻译一下就是,不要越俎代庖,不是你的东西你别乱碰,欠儿欠儿的。</p>
<p><img src="/img/remote/1460000006930017?w=569&h=553" alt="image" title="image"></p>
<p>所以这个方案不会像方案二那样随心所欲的使用,但其实也基本够用了。</p>
<p>没有的实例方法可以采用方案三委屈下。</p>
<p>个人还是比较推荐这两种合体的方案。</p>
<p>需要注意的一点是:</p>
<p>开启 polyfill 后,会与 <code>export * from 'xx'</code> 有冲突</p>
<p>请看构建后的代码:</p>
<pre><code class="javascript">...
/***/ },
/* 106 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
// 这是什么鬼。
import _Object$defineProperty from 'babel-runtime/core-js/object/define-property';
import _Object$keys from 'babel-runtime/core-js/object/keys';
Object.defineProperty(exports, "__esModule", {
value: true
});
...</code></pre>
<p>截止 2016-09-10,官方尚未解决此 <a href="https://link.segmentfault.com/?enc=Nkab3QpItWC%2FxaNYmTJa0g%3D%3D.YsoLtI5adxyo%2FmuyQNNVXIY17ke8ZtrtmAEvcDAjSAmQRv8FsyaAM0RY%2FDLRr5IrBhYKwWRYJMwgHQmpkDwFFw%3D%3D" rel="nofollow">issue</a>, 只有先避开 <code>export * from 'xx'</code> 这种写法。或在<a href="https://link.segmentfault.com/?enc=YBHAbAxRHMdX%2Bf2jc1gnIQ%3D%3D.NyJBObMscn%2FkDRbhsssynDzTYawbRAzDSKWJhbl4vkqu6BBZKkPLqXRMVkF9V9XwCKNKilZRGE2XRO0%2BNVo%2Bfy0ex7SAKNID1gJq0CTou4Ys36Kf2i2L2VpSatB7OrIevnIB%2FLQ0zcT%2FSjMwDlIM9m6tt4CO1lx9wEitDcTAR5wcwGnhTQr3tghmGP%2BH6FMY1fzD00%2BWE%2FfaIYJQrNxLLg%3D%3D" rel="nofollow">这里</a>找答案。</p>
<ul><li><p>第三件,是会引入一些 helper 来代替每次都生成的通用函数,看个例子就明白了</p></li></ul>
<p>原来构建好的代码每个模块都有类似这种代码:</p>
<pre><code class="javascript">function _classCallCheck(instance, Constructor)...
function _possibleConstructorReturn(self, call)...
function _inherits(subClass, superClass)...
</code></pre>
<p>开启 helper 后:</p>
<pre><code class="javascript">var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
var _inherits2 = require('babel-runtime/helpers/inherits');</code></pre>
<p>这样统一引用了 helper,去处了冗余,看起来也更优雅了。</p>
<p>在 v6.12.0 之前 helper 也是默认开启的,没有配置可改,其他的 ployfill regenerator 都是有配置可以设置的。也许是推荐你使用 helper 。</p>
<p>但是 v6.12.0 (2016-07-27) 增加了 helper 的配置。为什么呢?</p>
<p>我最开始用这个插件的时候也很诧异,按道理来说,去除了冗余代码,代码的体积应该变小才对,但实际测试却变大了,我测试时是未经 uglify 的代码从 18k 增加到了 78k,查看构建模块增加了将近 100 个 <a href="https://link.segmentfault.com/?enc=RaCHt6VOzyyliIzi4trGjQ%3D%3D.no2sm0OGWH2QTk7sm6zhIPVMIbkXoVzNl6yrwMgIu34AOhp%2Fu%2B8wjyOjbCU5BoqxrCTuRpqU94Phh1GcKsoi7qu5Fo6s9%2BAI%2BINHE0kMAw7Ch9%2B3eWpb%2FNZfs%2B5ShPcMN0Ez8fQhnq8gaHtKNJ%2Fn98av%2Fr20ABWX1AB8j7mfNVY%3D" rel="nofollow">详情</a>。</p>
<p>原因是从 babel-runtime 里引入的 helper 依赖很多,全部都是兼容最底层的。比如 <code>Object.create</code> <code>typeof</code> 这种方法全部被重写了。</p>
<p>后来 gaearon 大神都忍不了了,他测试的结果是增加了 5kB min+gzip <a href="https://link.segmentfault.com/?enc=dGNijDxaRT0305wOnxP3gQ%3D%3D.NIEevLBscQEYlDvaLpd8Tbi8GFwoAivNuYw1MQiT0uXAv735mwwqBQEI%2FvBw3LwgrHf5O9NS%2BAbJ5%2FVEY6SHBw%3D%3D" rel="nofollow">详情</a>。</p>
<p>于是有了 helper 这个配置项。</p>
<p>另外还有一点,如果开启了 helper 的话,你会发现之前引用的 <a href="https://link.segmentfault.com/?enc=SHtZUpq4cOmrZOzLYE2pOA%3D%3D.b0sf1EWT0v9hBjx3%2FEeeM0dALzHjG8UyindFLdZMFyZUza81vb5yRKIOOp%2BQb4Vw8jDTa7O%2B3YX0GLmN7Gl9%2BJSnzDyJXDnGRxmBaSS2pLs8wAW7uK10w5Qz7zXzrqWV" rel="nofollow">babel-plugin-transform-proto-to-assign</a> 就失效了,虽然他本来就不该被使用,后面会讲到。</p>
<p>所以目前看来这个 helper 不用也罢。</p>
<p>再说下 moduleName 这个参数是干什么的?</p>
<p>还记得开启 helper 后的代码吗</p>
<pre><code class="javascript">var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');</code></pre>
<p>看下这个路径,如果是本地项目安装了 babel-runtime 是没问题的,但如果你是用的通用构建工具,比如 <a href="https://link.segmentfault.com/?enc=IAPtoAmsjU74fkYis3aT5A%3D%3D.5tbUpNYTsYC804MYBgOOIVr7TEB%2BCNiLaHtaJKrAa0PVDJb%2Bd5Oa4QtY%2FL1Hd9FJ" rel="nofollow">nowa</a>,所有的构建依赖库都是在公共的地方,毕竟 babel 太太了。这里就会报错了。</p>
<pre><code>Cannot resolve module babel-runtime/regenerator</code></pre>
<p>gaearon 大神在写 <a href="https://link.segmentfault.com/?enc=G72kAbgslBbwLeul7M7EQQ%3D%3D.BMfc%2F5iRM4K4TwLMT0v6o2HxqiGvQ%2Bj%2FxFu8xaWgSAHpddGTjX7UhU05%2B8NJDoZrt4IAj2G2WqgWkIA7o2wgbQ%3D%3D" rel="nofollow">create-react-app</a> 时也发现了这个问题, <a href="https://link.segmentfault.com/?enc=4qXZ9iA51KkXExZxZWvFeQ%3D%3D.M0hGAFegzJ9tV2AmA7zvoP%2FVs94ImKplWa%2BKLrakg5l6Ln0T2dAOp6qzSSzRrsUc2tsyejLs76SyCxVjo8R5%2BJYIsiogqN8g33kgOV3mGv4%3D" rel="nofollow">详情</a></p>
<p>虽然这个问题可以通过 webpack 的 resolve.root 来解决,但是 gaearon 大神看其不爽,觉得依赖 webpack 不够优雅,<a href="https://link.segmentfault.com/?enc=Owf2c43KXiyDTQzTcLZUHA%3D%3D.gKHs0GmCPW%2BZAgf%2B3446PK%2BVa3q7GnnQXUxh%2BGpM8GZtr07hkRBXpm3Ph0mrd5XC" rel="nofollow">#3612</a> 于是乎就有了 moduleName 这个参数,已发布 v6.15.0 (2016-08-31)。</p>
<h2>放弃 loose 模式, 放弃 ie8</h2>
<p>上篇中提到了开启了 loose 模式来解决低版本浏览器无法继承到在构造函数里定义的属性或方法。</p>
<p>我们是通过 <code>babel-preset-es2015-ie</code> 这个插件,主要是改写了 <code>babel-plugin-transform-es2015-classes: {loose: true}</code> 和添加了插件 <code>babel-plugin-transform-proto-to-assign</code>(解决类方法继承的问题)</p>
<p>在 <code>babel-preset-es2015</code> v6.13.0 (2016-08-04) 时,presets 已经支持了参数配置,可以直接开启 loose 模式。</p>
<p>它内部会把开启一些插件的 loose 模式,不只是<code>babel-plugin-transform-es2015-classes</code></p>
<pre><code class="javascript">{
presets: [
["es2015", { "loose": true }]
]
}</code></pre>
<p>这样我们就可以直接使用 <code>babel-preset-es2015</code>,至于 <code>babel-plugin-transform-proto-to-assign</code> 可以单独配置,也可不使用,因为类方法本来就不该被继承,要使用就直接 <code>Parent.defaultProps</code> 就可以了。</p>
<p>在上文中并没有提到开启 loose 模式的另一个原因是解决 ie8 下的两个 es3 属性名关键字的问题,因为上文测试均在 ie9 上,所以上述的方案也是停留在必须支持 ie8。</p>
<p>那么如果我们放弃了 ie8 ,看一看是不是会海阔天空。</p>
<p>在 <code>babel-plugin-transform-es2015-classes</code> v6.14.0 (2016-08-23) 一个 ‘大胡子哥’(原谅我不认识他) 修复了 <code>__proto__ </code>这个问题 <a href="https://link.segmentfault.com/?enc=tIIIre7Kq6tYTFq8zzlhBg%3D%3D.HK28voqvVhYvBowVtnfSdlN0fBJnA83mJ13ZbMUvcqDr8qJGNEv1TRTYLpxpmvrP" rel="nofollow">#3527 Fix class inheritance in IE <=10 without loose mode.</a> <br>这样我们就可以在 ie9+ 上使用正常的 es6 模式了。</p>
<p>毕竟我们该向前看,loose 模式有点后退的赶脚。</p>
<p><a href="https://link.segmentfault.com/?enc=lk5a8pf0NXi6G%2BcD57pr4Q%3D%3D.UMHUG8LtNilN66he0JRmwCb0KisNubg9HJxqo78yApU1kfdsaeyeqg241DiZIUo0hwzH9BY8aWypMPLXXdgO0Y4R0w5C9Wp5Zlz88qDo8uldTja2OJuiG6G%2BOlnGFjxB" rel="nofollow">这篇文章</a>也表达了不推荐使用 loose 模式</p>
<blockquote><p>Con: You risk getting problems later on, when you switch from transpiled ES6 to native ES6. That is rarely a risk worth taking.</p></blockquote>
<p>当然,如果真的离不开 ie8,就针对 es3 关键字的问题引用两个插件即可</p>
<pre><code class="javascript">require('babel-plugin-transform-es3-member-expression-literals'),
require('babel-plugin-transform-es3-property-literals'),</code></pre>
<p>我们再稍微看下‘大胡子哥’的修改,其实很简单,也很巧妙,看一行关键代码</p>
<pre><code class="javascript">// 修改后生成的代码多了一个 先取 `xxx.__proto__` 再使用 `Object.getPrototypeOf`
var _this = _possibleConstructorReturn(this, (Test.__proto__ || Object.getPrototypeOf(Test)).call(this, props));
</code></pre>
<p>回顾下 inherits 方法的实现</p>
<pre><code class="javascript">function _inherits(subClass, superClass) {
...
// 虽然 ie9/10 不支持 `__proto__`,这里只是作为了普通对象给予赋值,`Object.getPrototypeOf` 获取不到但可以直接 `.__proto__` 获取
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
...</code></pre>
<p>如果你看懂了实现方式,不知道你有没有发现 <code>babel-plugin-transform-proto-to-assign</code>(解决类方法继承的问题)这个家伙真的不能用了</p>
<pre><code class="javascript">function _inherits(subClass, superClass) {
...
// 因为它会将 `__proto__` 转为 `_default`
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : _defaults(subClass, superClass);
}</code></pre>
<p>这样上述的修复就无效了。切记不能使用,还是那句话,类方法本来就不该被继承。</p>
<p>最后看下终极方案的通用配置</p>
<pre><code class="javascript">{
plugins: [
["transform-runtime", {
"helpers": false,
"polyfill": true,
"regenerator": true
}],
'add-module-exports',
'transform-es3-member-expression-literals',
'transform-es3-property-literals',
],
"presets": [
'react',
'es2015',
'stage-1'
],
}</code></pre>
<p>更简单、完整的解决方案,请查看 <a href="https://link.segmentfault.com/?enc=YlpW5dB%2BlPepmK8y3XU2cg%3D%3D.r8UEKWMJ1MJL1Wrjy2XBJwXH%2FxAfo1%2BMSPVviWF2H2P432wEfGgRU2o1w6M04c3i" rel="nofollow">nowa</a></p>
<p>感谢阅读。</p>
<h2>参考链接</h2>
<ul>
<li><p><a href="https://link.segmentfault.com/?enc=%2FUBaez5EJmQzhjBXGQDL1w%3D%3D.RIy%2B02pEstXRCDjG7RqNiZA2H3XND5iA0mfR%2FG2aK6f5JDRBx2JEOyOZ50V6A86cH9tXOwx0u3R84gGeOUDnVg%3D%3D" rel="nofollow">https://babeljs.io/docs/plugi...</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=Xm9SADmVx3W7b90F%2FGNHGQ%3D%3D.SQoW2gHO6RAGbn6Hx9%2FYWeMvT%2FV4deyc6iLJgaLvxlw%3D" rel="nofollow">https://github.com/babel/babel</a></p></li>
</ul>
<h2>广告时间: 请献出你的小星星</h2>
<ul>
<li><p><a href="https://link.segmentfault.com/?enc=YIryR01dQ2XryWVayKBHoQ%3D%3D.FKgObHo7RMSz47%2B%2Fea9QLoeIishhrl4tI%2BY%2F7pod8SdQv5GC0kiuW%2FoV3FZ%2BIaJJ" rel="nofollow">https://github.com/saltjs/sal...</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=69YKj91fkRZngPUurZRVWg%3D%3D.vSy0qoz2BnC3Fa1NpGEaelDNt5k8wCGm8VaHSBTUyhFQS8eWDPoQQLXIVu80d1zP" rel="nofollow">https://github.com/uxcore/uxcore</a></p></li>
</ul>
ES6 + Webpack + React + Babel 如何在低版本浏览器上愉快的玩耍(上)
https://segmentfault.com/a/1190000006929961
2016-09-18T14:56:11+08:00
2016-09-18T14:56:11+08:00
matianming
https://segmentfault.com/u/maming
13
<h2>起因</h2>
<p>某天,某测试说:“这个页面在 IE8 下白屏,9也白。。”</p>
<p>某前端开发: 吭哧吭哧。。。一上午的时间就过去了,搞定了。</p>
<p>第二天,某测试说:“IE 又白了。。”</p>
<p>某前端开发: 吭哧吭哧。。。谁用的 <code>Object.assign</code>,出来我保证削不屎你。</p>
<p><img src="/img/remote/1460000006929964?w=458&h=231" alt="" title=""></p>
<p>原谅我不禁又黑了一把 IE。</p>
<p>有人可能会想,都要淘汰了,还有什么好讲的?</p>
<p>也许几年后,确实没用了,但目前我们的系统还是要对 ie8+ 做兼容,因为确实还有个别用户,尽管他没朋友。。。</p>
<p>记录下本次在 IE 下踩得坑,让后面的同学能够不再在这上面浪费时间了。</p>
<h2>经过</h2>
<h3>测试</h3>
<p>首先,看下面代码(以下测试在 IE9)</p>
<pre><code class="javascript">class Test extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div>{this.props.content}</div>;
}
}
module.exports = Test;</code></pre>
<p>这段代码跑的妥妥的,没什么问题。</p>
<p>一般来说,babel 在转换继承时,可能会出现兼容问题,那么,再看这一段</p>
<pre><code class="javascript">class Test extends React.Component {
constructor(props) {
super(props);
}
test() {
console.log('test');
}
render() {
return <div>{this.props.content}</div>;
}
}
Test.defaultProps = {
content: "测试"
};
class Test2 extends Test {
constructor(props) {
super(props);
this.test();
}
}
Test2.displayName = 'Test2';
module.exports = Test2;</code></pre>
<p>这段代码同样也可以正常运行</p>
<p>也就是说在上述这两种情况下,不做任何处理(前提是已经加载了 es5-shim/es5-sham),在 IE9 下都可以正常运行。</p>
<p>然后我们再看下会跑挂的代码</p>
<pre><code class="javascript">class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
test: 1,
};
}
test() {
console.log(this.state.value);
}
render() {
return <div>{this.props.content}</div>;
}
}
Test.defaultProps = {
content: "测试"
};
class Test2 extends Test {
constructor(props) {
super(props);
// SCRIPT5007: 无法获取属性 "value" 的值,对象为 null 或未定义
this.test();
// SCRIPT5007: 无法获取属性 "b" 的值,对象为 null 或未定义
this.a = this.props.b;
}
}
// undefined
console.log(Test2.defaultProps);
Test2.displayName = 'Test2';
module.exports = Test2;</code></pre>
<p>这段代码在高级浏览器中是没问题的,在 IE9 中会出现注释所描述的问题</p>
<p>从这些问题分析,可得出3个结论</p>
<ol>
<li><p>在构造函数里的定义的属性无法被继承</p></li>
<li><p>在构造函数里不能使用 <code>this.props.xx</code></p></li>
<li><p>类属性或方法是无法被继承的</p></li>
</ol>
<p>也就是说,只要规避了这三个条件的话,不做任何处理(前提是已经加载了 es5-shim/es5-sham),在 IE9 下都可以正常运行。</p>
<blockquote>
<p>第二点,是完全可以避免的,切记在 <code>constructor</code> 直接使用 <code>props.xxx</code>, 不要再用 <code>this.props.xxx</code></p>
<p>第三点,也是可以完全避免的,因为从理论上来说,类属性就不该被继承,如果想使用父类的类属性可以直接<code>Test2.defaultProps = Test.defaultProps;</code></p>
<p>第一点,可避免,但无法完全避免</p>
</blockquote>
<h3>原因</h3>
<p>第一点,有时是无法完全避免的,那么就要查询原因,才能找到解决方案</p>
<p>我们把 babel 转义后的代码放出来就能查出原因了</p>
<pre><code class="javascript">'use strict';
var _createClass = function () {
...
}();
function _classCallCheck(instance, Constructor) {
...
}
function _possibleConstructorReturn(self, call) {
...
// 这个方法只是做了下判断,返回第一个或第二参数
return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) {
...;
// 这里的 _inherits 是通过将子类的原型[[prototype]]指向了父类,所以如果在高级浏览器下,子类的可以继承到类属性
// 根本问题也是出在这里,IE9 下既没有 `setPrototypeOf` 也没有 `__proto__`
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
var Test = function (_React$Component) {
...
return Test;
}(React.Component);
Test.defaultProps = {
content: "测试"
};
var Test2 = function (_Test) {
_inherits(Test2, _Test);
function Test2(props) {
_classCallCheck(this, Test2);
// 这里的 this 会通过 _possibleConstructorReturn,来获取父类构造函数里定义的属性
// _possibleConstructorReturn 只是做了下判断,如果第二个参数得到了正确执行,则返回执行结果,否则返回第一个参数,也就是子类的 this
// 也就是说问题出在 Object.getPrototypeOf
// 在 _inherits 中将子类的原型指向了父类, 这里通过 getPrototypeOf 来获取父类,其实就是 _Test
// Object.getPrototypeOf 不能正确的执行,导致了子类无法继承到在构造函数里定义的属性或方法,也无法继承到类属性或方法
var _this2 = _possibleConstructorReturn(this, Object.getPrototypeOf(Test2).call(this, props));
_this2.test();
console.log(_this2.props.children);
return _this2;
}
return Test2;
}(Test);
console.log(Test2.defaultProps);
Test2.displayName = 'Test';
module.exports = Test2;</code></pre>
<p>通过上述的代码注释,可以得出有两处问题需要解决</p>
<ol>
<li><p>正确的获取父类(解决无法继承到在构造函数里定义的属性或方法)</p></li>
<li><p>正确的将子类的原型指向了父类(解决无法继承到类属性或方法)</p></li>
</ol>
<h3>解决方案</h3>
<p>通过文档的查询,发现只要开启 es2015-classes 的 loose 模式即可解决第一个问题</p>
<h4>loose 模式</h4>
<blockquote>
<p>Babel have two modes:</p>
<ul>
<li><p>A normal mode follows the semantics of ECMAScript 6 as closely as possible.</p></li>
<li><p>A loose mode produces simpler ES5 code.</p></li>
</ul>
</blockquote>
<p>Babel 有两种模式:</p>
<ul>
<li><p>尽可能符合 ES6 语义的 normal 模式。</p></li>
<li><p>提供更简单 ES5 代码的 loose 模式。</p></li>
</ul>
<p>尽管官方是更推荐使用 normal 模式,但为了兼容 IE,我们目前也只能开启 loose 模式。</p>
<p>在 babel6 中,主要是通过 babel-preset-2015 这个插件,来进行转义的<br>我们看下 babel-preset-2015</p>
<pre><code class="javascript"> plugins: [
require("babel-plugin-transform-es2015-template-literals"),
require("babel-plugin-transform-es2015-literals"),
require("babel-plugin-transform-es2015-function-name"),
...
require("babel-plugin-transform-es2015-classes"),
...
require("babel-plugin-transform-es2015-typeof-symbol"),
require("babel-plugin-transform-es2015-modules-commonjs"),
[require("babel-plugin-transform-regenerator"), { async: false, asyncGenerators: false }],
]</code></pre>
<p>是一堆对应转义的插件,从命名上也可看出了大概,比如 babel-plugin-transform-es2015-classes 就是做类的转义的,也就是我们只需把它开启 loose 模式,即可解决我们的一个问题</p>
<pre><code class="javascript">[require('babel-plugin-transform-es2015-classes'), {loose: true}],</code></pre>
<p>看下开启了 loose 模式的代码,你会发现它的确更接近 ES5</p>
<pre><code class="javascript">var Test = function (_React$Component) {
...
// 这里是 ES5 的写法
Test.prototype.test = function test() {
console.log(this.state.value);
};
/* normal 模式是这样的
{
key: 'test',
value: function test() {
console.log(this.state.value);
}
}
*/
return Test;
}(React.Component);
var Test2 = function (_Test) {
_inherits(Test2, _Test);
function Test2(props) {
_classCallCheck(this, Test2);
// 这里直接拿到了父类 _Test, 即解决了无法继承到在构造函数里定义的属性或方法
var _this2 = _possibleConstructorReturn(this, _Test.call(this, props));
_this2.test();
return _this2;
}
return Test2;
}(Test);</code></pre>
<p>我们可以通过去安装 <a href="https://link.segmentfault.com/?enc=6ZLmKV%2F7NNwCUfCeASqDHg%3D%3D.txmM2y%2FmmkL%2BQYGrg7yXYQt1yQ8Ft9VybB6H6eDQdNklgej%2FUmCpZxvZd3LEL8jLYVcRsalM24jVyCHDKpE3nw%3D%3D" rel="nofollow">babel-preset-es2015-loose</a>, 这个插件来开启 loose 模式。</p>
<p>但从我们团队的 <strong>老司机</strong> 口中</p>
<p><img src="/img/remote/1460000006929965" alt="image" title="image"></p>
<p>得到了一个更好插件<a href="https://link.segmentfault.com/?enc=VN04TiLtp194tj2vM%2BDAUQ%3D%3D.QUibsSIDu%2FYlHy6Ebxfk1hNnW7gMxhTuGzR1%2B9rHJLVi%2FE9l77fVGmD948c%2B8njrJ%2BPuQZIA2%2BJ92iUyfmadHA%3D%3D" rel="nofollow">babel-preset-es2015-ie</a>,看下这个插件的代码,发现它和原来的 babel-preset-2015 只有两行区别</p>
<pre><code class="javascript">[
[require('babel-plugin-transform-es2015-classes'), {loose: true}],
require('babel-plugin-transform-proto-to-assign'),
]</code></pre>
<p>刚好解决我们上述碰到的两个问题</p>
<p>这个 <code>babel-plugin-transform-proto-to-assign</code> 插件会生成一个 _defaults 方法来处理原型</p>
<pre><code class="javascript">function _inherits(subClass, superClass) {
...;
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : _defaults(subClass, superClass);
}</code></pre>
<pre><code class="javascript">function _defaults(obj, defaults) {
var keys = Object.getOwnPropertyNames(defaults);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = Object.getOwnPropertyDescriptor(defaults, key);
if (value && value.configurable && obj[key] === undefined) {
Object.defineProperty(obj, key, value);
}
}
return obj;
}
</code></pre>
<blockquote><p>这个插件正确的将子类的原型指向了父类(解决无法继承到类属性或方法)</p></blockquote>
<h2>总结</h2>
<p>本文讲述低版本浏览器报错的原因和解决方案</p>
<ul>
<li><p>一方面是提示下在构造函数里不要使用 <code>this.props.xx</code></p></li>
<li><p>另一方面也对继承的机制有了更好的理解</p></li>
</ul>
<p>在这次项目中发现在低版本浏览器跑不起来的两点主要原因:</p>
<ol>
<li><p><code>SCRIPT5007: 无法获取属性 xxx 的值,对象为 null 或未定义</code>,这种情况一般是组件继承后,无法继承到在构造函数里定义的属性或方法,同样类属性或方法也同样无法继承</p></li>
<li><p><code>SCRIPT438: 对象不支持 xxx 属性或方法</code>,这种情况一般是使用了 es6、es7 的高级语法,<code>Object.assgin</code> <code>Object.keys</code> 等,这种情况在移动端的一些 ‘神机’ 也一样会挂。</p></li>
</ol>
<p>第一点本文已经分析,预知第二点讲解请见下篇。</p>
<p>备注:下篇会主要介绍下如何让 用了 <code>Object.assign</code> 的那位同学可以继续用,又不会被削。</p>
如何在 React 中做到 jQuery-free
https://segmentfault.com/a/1190000006201488
2016-08-08T10:49:30+08:00
2016-08-08T10:49:30+08:00
紅白
https://segmentfault.com/u/eternalsky
10
<h2>前言</h2>
<p>前些天在订阅的公众号中看到了以前阮一峰老师写过的一篇文章,<a href="https://link.segmentfault.com/?enc=x7WACQ0QhGYiHhIR8zoOFw%3D%3D.O5MC7AnnJEnTCt05pTBNsEgzd%2BhWYgHFdgjCkBRd%2FOvIo7fa1%2BuBkBoS%2BAPG5jwi7emL2mPKSH3VLvIKk9qKiA%3D%3D" rel="nofollow">「如何做到 jQuery-free?」</a>。这篇文章讨论的问题,在今天来看仍不过时,其中的一些点的讨论主要是面向新内核现代浏览器的标准 DOM API,很可惜的是在目前的开发环境下,我们仍然无法完全抛弃 IE,大部分情况下我们至少还要兼容到 IE 8,这一点使我们无法充分践行文章中提到的一些点,而本文也正是首次启发,顺着阮老师文章的思路来讨论如何在 React 中实战 IE8-compatible 的 jQuery-free。</p>
<p>首先我们仍要说的是,jQuery 是现在最流行的 JavaScript 工具库。在 W3techs 的<a href="https://link.segmentfault.com/?enc=TzUUX85yjIkW%2Br54V8rgfA%3D%3D.vWSdQgOZ3aSSteLLALXzMqlUBMt8J8XZRj5oFYEne0HV33bYvUyrtszq%2FakLAqsZPHjNUp%2Fuu1ATxURD1ctQ0RclTx%2B0PZqUBsbn6%2FhvMEk%3D" rel="nofollow">统计</a>中,目前全世界 70.6% 的网站在使用他,而 React 甚至还不到 0.1%,但 React 一个值得注意的趋势是,他在目前顶级流量网站中的使用率是最高的,比例达到了 <a href="https://link.segmentfault.com/?enc=utjoCyuagAHiOipjl26Acw%3D%3D.syBS4kYQbm08e%2FE7RXZR8mJ%2BNfmVKp3AHFqLqzFPgH4HE1FGI%2BKz22YsfiUFLSnRn0oJIyRmUrLGhe1b3PmgNA%3D%3D" rel="nofollow">16%</a>。这一趋势也表明了目前整个前端界的技术趋势,但 70.6% 的数字也在告诉我们,jQuery 在 JS 库中的王者地位,即使使用了React,也可能因为各种各样的原因,还要和 jQuery 来配合使用。但 React 本身的体积已经让我们对任何一个重库产生了不适反应,为了兼容 IE8,我们仍然需要使用 1.x 的 jQuery 版本,但当时设计上的缺陷使得我们无法像 lodash 那样按需获取。而 React 和 jsx 的强大,又使得我们不需要了 jQuery 的大部分功能。从这个角度来看,他臃肿的体积让开发者更加难以忍受,jQuery-free 势在必行。</p>
<p>下面就顺着阮老师当年的思路,来讨论如何使用 React 自带的强大功能,和一些良心第三方库屏蔽兼容性,来取代 jQuery 的主要功能,做到 jQuery-free。</p>
<p>(注:React 15.x 版本已经不再兼容 IE8,因此本文讨论的 React 仍是 0.14.x 的版本,同时为了易于理解,本文也基本上以 ES6 class 的方式来声明组件,而不采用 pure function。)</p>
<h2>一、选取 DOM 元素</h2>
<p>在 jQuery 中,我们已经熟悉了使用 sizzle 选择器来完成 DOM 元素的选取。而在 React 中,我们可以使用 <a href="https://link.segmentfault.com/?enc=vq0DOBByM2Bil98xLAiIPA%3D%3D.wcIV39YXPcNtJQH3TYtBLUjfgt9js0A39eRTpgtbHbDKMYlbStEEfaBhRqhuwXi3fhKVPvWHxgQhg1RTbMEsfw%3D%3D" rel="nofollow">ref</a> 来更有针对性的获取元素。</p>
<pre><code class="js">import React from 'react';
class Demo extends React.Compoent {
getDomNode() {
return this.refs.root; // 获取 Dom Node
}
render() {
return (
<div ref="root">just a demo</div>
);
}
}</code></pre>
<p>这是最简单的获取 node 的方式,如果有多层结构嵌套呢?没有关系。</p>
<pre><code class="js">import React from 'react';
class Demo extends React.Compoent {
getRootNode() {
return this.refs.root; // 获取根节点 Dom Node
}
getLeafNode() {
return this.refs.leaf; // 获取叶节点 Dom Node
}
render() {
return (
<div ref="root">
<div ref="leaf">just a demo</div>
</div>
);
}
}</code></pre>
<p>如果是组件和组件嵌套呢?也没关系,父组件仍然可以拿到子组件的根节点。</p>
<pre><code class="js">import React from 'react';
import ReactDOM from 'react-dom';
class Sub extends React.Compoent {
render() {
return (
<div>a sub component</div>
);
}
}
class Demo extends React.Compoent {
getDomNode() {
return this.refs.root; // 获取 Dom Node
}
getSubNode() {
return ReactDOM.findDOMNode(this.refs.sub); // 获取子组件根节点
}
render() {
return (
<div ref="root">
<Sub ref="sub" />
</div>
);
}
}
</code></pre>
<p>上面使用了比较易懂的 API 来解释 Ref 的用法,但里面包含了一些现在 React 不太推荐和即将废弃的方法,如果用 React 推荐的写法,我们可以这样写。</p>
<pre><code class="js">import React from 'react';
import ReactDOM from 'react-dom';
class Sub extends React.Compoent {
getDomNode() {
return this.rootNode;
}
render() {
return (
<div ref={(c) => this.rootNode = c}>a sub component</div>
);
}
}
class Demo extends React.Compoent {
getDomNode() {
return this.rootNode; // 获取 Dom Node
}
getSubNode() {
return this.sub.getDomNode(); // 获取子组件根节点
}
render() {
return (
<div ref={(c) => this.rootNode = c}>
<Sub ref={(c) => this.sub = c} />
</div>
);
}
}
</code></pre>
<p>有人可能会问,那子组件怎么拿父组件的 Dom Node 呢,从 React 的单向数据流角度出发,遇到这种情况我们应该通过回调通知给父组件,再由父组件自行判断如何修改 Node,其实父组件拿子组件的 Node 情况也很少,大多数情况下我们是通过 props 传递变化给子组件,获取子组件 Node,更多的情况下是为了避开大量重新渲染去修改一些Node的属性(比如 scrollLeft)。</p>
<h2>二、DOM 操作</h2>
<p>jQuery 中提供了丰富的操作方法,但一个个操作 DOM 元素有的时候真的很烦人并且容易出错。React 通过数据驱动的思想,通过改变 view 对应的数据,轻松实现 DOM 的增删操作。</p>
<pre><code class="js">class Demo extends React.Compoent {
constructor(props) {
super(props);
this.state = {
list: [1, 2, 3],
};
this.addItemFromBottom = this.addItemFromBottom.bind(this);
this.addItemFromTop = this.addItemFromTop.bind(this);
this.deleteItem = this.deleteItem.bind(this);
}
addItemFromBottom() {
this.setState({
list: this.state.list.concat([4]),
});
}
addItemFromTop() {
this.setState({
list: [0].concat(this.state.list),
});
}
deleteItem() {
const newList = [...this.state.list];
newList.pop();
this.setState({
list: newList,
});
}
render() {
return (
<div>
{this.state.list.map((item) => <div>{item}</div>)}
<button onClick={this.addItemFromBottom}>尾部插入 Dom 元素</button>
<button onClick={this.addItemFromTop}>头部插入 Dom 元素</button>
<button onClick={this.deleteItem}>删除 Dom 元素</button>
</div>
);
}
}</code></pre>
<h2>三、事件的监听</h2>
<p>React 通过根节点代理的方式,实现了一套很优雅的事件监听方案,在组件 unmount 时也不需要自己去处理内存回收相关的问题,非常的方便。</p>
<pre><code class="js">import React from 'react';
class Demo extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
alert('我是弹窗');
}
render() {
return (
<div onClick={this.handleClick}>点击我弹出弹框</div>
);
}
}</code></pre>
<p>这里有一个小细节就是 bind 的时机,bind 是为了保持相应函数的上下文,虽然也可以在 onClick 那里 bind,但这里选择在 constructor 里 bind 是因为前者会在每次 render 的时候都进行一次 bind,返回一个新函数,是比较消耗性能的做法。</p>
<p>但 React 的事件监听,毕竟只能监听至 root component,而我们在很多时候要去监听 window/document 上的事件,如果 resize、scroll,还有一些 React 处理不好的事件,比如 scroll,这些都需要我们自己来解决。事件监听为了屏蔽差异性需要做很多的工作,这里像大家推荐一个第三方库来完成这部分的工作,<a href="https://link.segmentfault.com/?enc=zU8Y6V3U1Jsm7hrkwQGn5w%3D%3D.PzNpUiw48G2GOHywRJ9Hzov24xaANnRh%2BYBgyX6C9M1tJ1YdA4tCkwM%2FQBmgzSWa5hvDxBu42I6UfeZunW9pcw%3D%3D" rel="nofollow">add-dom-event-listener</a>,用法和原生的稍有区别,是因为这个库并不旨在做 polyfill,但用法还是很简单。</p>
<pre><code class="js">var addEventListener = require('add-dom-event-listener');
var handler = addEventListener(document.body, 'click', function(e){
console.log(e.target); // works for ie
console.log(e.nativeEvent); // native dom event
});
handler.remove(); // detach event listener</code></pre>
<p>另一个选择是 <a href="https://link.segmentfault.com/?enc=OFleVf%2FMfW2tsimGGa3vCw%3D%3D.OHiXd83Kh%2FJEe1AlKuhL22zfVQEdtf4SlZxYWdXlfG0%3D" rel="nofollow">bean</a>,达到了 IE6+ 级别的兼容性。</p>
<h2>四、事件的触发</h2>
<p>和事件监听一样,无论是 Dom 事件还是自定义事件,都有很优秀的第三方库帮我们去处理,如果是 DOM 事件,推荐 <a href="https://link.segmentfault.com/?enc=A1aLqyIecRuww9lSkzujXg%3D%3D.KcnVbYgxQR%2FEigOacyy%2Bx8%2BT7wOfaYj8GxLnn%2F9bwNo%3D" rel="nofollow">bean</a>,如果是自定义事件的话,推荐 <a href="https://link.segmentfault.com/?enc=x8ASM%2BtCx3liiJW5NgVzVQ%3D%3D.adBsq%2Fj2%2BVRrLkQ2JUpOLgItEwK5s5y74k%2BKLclP8X6wweYRpnYNzGkL2eJYBzBd" rel="nofollow">PubSubJS</a>。</p>
<h2>五、document.ready</h2>
<p>React 作为一个 view 层框架,通常情况下页面只有一个用于渲染 React 页面组件的根节点 div,因此 document.ready,只需把脚本放在这个 div 后面执行即可。而对于渲染完成后的回调,我们可以使用 React 提供的 componentDidMount 生命周期。</p>
<pre><code class="js">import React from 'react';
class Demo extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
doSomethingAfterRender(); // 在组件渲染完成后执行一些操作,如远程获取数据,检测 DOM 变化等。
}
render() {
return (
<div>just a demo</div>
);
}
}</code></pre>
<h2>六、attr 方法</h2>
<p>jQuery 使用 attr 方法,获取 Dom 元素的属性。在 React 中也可以配合 Ref 直接读取 DOM 元素的属性。</p>
<pre><code class="js">import React from 'react';
class Demo extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.rootNode.scrollLeft = 10; // 渲染后将外层的滚动调至 10px
}
render() {
return (
<div
ref={(c) => this.rootNode = c}
style={{ width: '100px', overflow: 'auto' }}
>
<div style={{ width: '1000px' }}>just a demo</div>
</div>
);
}
}</code></pre>
<p>但是,在大部分的情况下,我们完全不需要做,因为 React 的单向数据流和数据驱动渲染,我们可以不通过 DOM,轻松拿到和修改大部分我们需要的 DOM 属性。</p>
<pre><code class="js">import React from 'react';
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
link: '//www.taobao.com',
};
this.getLink = this.getLink.bind(this);
this.editLink = this.editLink.bind(this);
}
getLink() {
alert(this.state.link);
}
editLink() {
this.setState({
link: '//www.tmall.com',
});
}
render() {
return (
<div>
<a href={this.state.link}>跳转链接</a>
<button onClick={this.getLink}>获取链接</button>
<button onClick={this.editLink}>修改链接</button>
</div>
);
}
}</code></pre>
<h2>七、addClass/removeClass/toggleClass</h2>
<p>在 jQuery 的时代,我们通常靠获取 Dom 元素后,再 <code>addClass/removeClass</code> 来改变外观。在 React 中通过数据驱动和第三库 <code>classnames</code> 修改样式从未如此轻松。</p>
<pre><code class="css">.fn-show {
display: block;
}
.fn-hide {
display: none;
}</code></pre>
<pre><code class="js">import React from 'react';
import classnames from 'classnames';
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
show: true,
};
this.changeShow = this.changeShow.bind(this);
}
changeShow() {
this.setState({
show: !this.state.show,
});
}
render() {
return (
<div>
<a
href="//www.taobao.com"
className={classnames({
'fn-show': this.state.show,
'fn-hide': !this.state.show,
})}
>
跳转链接
</a>
<button onClick={this.changeShow}>改变现实状态</button>
</div>
);
}
}</code></pre>
<h3>八、css</h3>
<p>jQuery 的 css 方法用于设置 DOM 元素的 style 属性,在 React 中,我们可以直接设置 DOM 的 style 属性,如果想改变,和上面的 class 一样,用数据去驱动。</p>
<pre><code class="js">import React from 'react';
class Demo extends React.Component {
constructor() {
super(props);
this.state = {
backgorund: 'white',
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({
background: 'black',
});
}
render() {
return (
<div
style={{
background: this.state.background,
}}
>
just a demo
<button>change Background Color</button>
</div>
);
}
}</code></pre>
<h2>九、数据存储</h2>
<p>比起 jQuery,React 反而是更擅长管理数据,我们没有必要像 jQuery 时那样将数据放进 Dom 元素的属性里,而是利用 state 或者 内部变量(this.xxx) 来保存,在整个生命周期,我们都可以拿到这些数据进行比较和修改。</p>
<h2>十、Ajax</h2>
<p>Ajax 确实是在处理兼容性问题上一块令人比较头疼的地方,要兼容各种形式的 Xhr 不说,还有 jsonp 这个不属于 ajax 的功能也要同时考虑,好在已经有了很好的第三方库帮我们解决了这个问题,这里向大家推荐 <a href="https://link.segmentfault.com/?enc=ci6wdFEJOxaHgPa3mKqyTg%3D%3D.0W%2BygCl09ecKPbk%2BO42Wk5iz0kArmD8SgROtTWnUFrTtqDkkVrOEu9tYKqx8O%2B1N" rel="nofollow">natty-fetch</a>,一个兼容 IE8 的fetch 库,在 API 设计上向 fetch 标准靠近,而又保留了和 jQuery 类似的接口,熟悉 $.ajax 应该可以很快的上手。</p>
<h2>十一、动画</h2>
<p>React 在动画方面提供了一个插件 <a href="https://link.segmentfault.com/?enc=OsSbzigu%2B7%2BjDrETFXxT4g%3D%3D.N4tgCpmWBpRlruEb9LhQgHIx9xHHc1GKM53nj4onjlwbTBWyFcOdcjH6vKoZnnP8kmGBrxDL6ROIudTH97wpY%2BIcNg4bnoj6AaDPvrAluIxZCXd43Ct9f0kXcYfdvFHl" rel="nofollow">ReactCSSTransitionGroup</a>,和它的低级版本 <a href="https://link.segmentfault.com/?enc=q7FLncYU7eLPGF6CAuFTCw%3D%3D.1ErSkJzpzw4tRHU0MZtgVeBY8iLngm%2Fu6%2Fr9mRZJwIJJHayEKRPajI%2FZc5IPdxY6YeE5FN5LZ8JTZVPmy3A5pjS0i8F22rQrRsWB2h43LzO%2Bs7q1dW0ipDNb%2BcoYE%2BL6" rel="nofollow">ReactTransitionGroup</a>,注意这里的低级并不是退化版本,而是更加基础的暴露更多 API 的版本。<br>这个插件的灵感来自于 Angular 的 ng-animate,在设计思路上也基本保持一致。通过指定 Transition 的类名,比如 <code>example</code> ,在元素进场和退场的时候分别加上对应的类名,以实现 CSS3 动画。例如本例中,进场会添加 <code>example-enter</code> 和 <code>example-enter-active</code> 到对应的元素 ,而在退场 <code>example-leave</code> 和 <code>example-leave-active</code> 类名。当然你也可以指定不同的进场退场类名。而对应入场,React 也区分了两种类型,一种是 ReactCSSTransitionGroup 第一次渲染时(appear),而另一种是 ReactCSSTransitionGroup 已经渲染完成后,有新的元素插入进来(enter),这两种进场可以使用 prop 进行单独配置,禁止或者修改超时时长。具体的例子,在上面给出的链接中有详细的例子和说明,因此本文不再赘述。</p>
<p>但这个插件最多只提供了做动画的方案,如果我想在动画进行的过程中做一些其他事情呢?他就无能为力了,这时候就轮到 ReactTransitionGroup 出场了。ReactTransitionGroup 为他包裹的动画元素提供了六种新的生命周期:<code>componentWillAppear(callback)</code>, <code>componentDidAppear()</code>, <code>componentWillEnter(callback)</code>, <code>componentDidEnter()</code>, <code>componentWillLeave(callback)</code>, <code>componentDidLeave()</code>。这些 hook 可以帮助我们完成一些随着动画进行需要做的其他事。</p>
<p>但官方提供的插件有一个不足点,动画只是在进场和出场时进行的,如果我的组件不是 mount/unmount,而只是隐藏和显示怎么办?这里推荐一个第三方库:<a href="https://link.segmentfault.com/?enc=%2Fi%2BzS3ULu1%2FeQrP15IaquQ%3D%3D.R0xrR8YI4OXjuh5oxnE80AeTdzWP5cb1ANygPX4%2BjFmiVWT%2FPlFe1K2PlzOfaMTa" rel="nofollow">rc-animate</a>,从 API 设计上他基本上是延续了 ReactCSSTransitionGroup 的思路,但是通过引入 <code>showProp</code> 这一属性,使他可以 handle 组件显示隐藏这一情况下的出入场动画(只要将组件关于 show/hide 的属性传给 showProp 即可),同时这个库也提供自己的 hook,来实现 appear/enter/leave 时的回调。</p>
<p>如果你说我并不满足只是进场和出场动画,我要实现类似鼠标拖动时的实时动画,我需要的是一个 js 动画库,这里向大家推荐一个第三方库:<a href="https://link.segmentfault.com/?enc=xsNzGfNULpC6V%2FyHg5nkiA%3D%3D.wBz99xDiirE%2BJyRx2cBucnpORSjMpngNbZIdSpI%2BHEKgd%2Fh9j%2BDTZADaVgyHkhax" rel="nofollow">react-motion</a> , react-motion 一个很大的特点是,有别以往使用贝塞尔曲线来定义动画节奏,引入了刚度和阻尼这些弹簧系数来定义动画,按照作者的说法,与其纠结动画时长和很难掌握的贝塞尔表示法,通过不断调整刚度和阻尼来调试出最想要的弹性效果才是最合理的。Readme 里提供了一系列的很炫的动画效果,比如这个 <a href="https://link.segmentfault.com/?enc=alBK%2Bdy1QOo6OAan50Dsag%3D%3D.L1TlxCglVuVuGz%2BCrNo0oAJz196I8kL4pFuOPqFk5e4d3EP%2Fcf3IRZuR1o8E3nqnjFWjAXHRjbbMCKY3zxVvpE8Q%2FHl3bgn5BAiZsPDHZTA%3D" rel="nofollow">draggable list</a>。Motion 通过指定 defaultStyle、style,传回给子组件正在变化中的 style,从而实现 js 动画。</p>
<pre><code><Motion defaultStyle={{x: 0}} style={{x: spring(10)}}>
{interpolatingStyle => <div style={interpolatingStyle} />}
</Motion></code></pre>
<h2>总结</h2>
<p>本文的灵感来源于阮老师两年前的文章,这篇的实战意义更大于对未来技术的展望,希望能够给各位正在使用 React 开发系统的同学们一点启发。</p>
<h2>参考链接</h2>
<ul>
<li><p>阮一峰,<a href="https://link.segmentfault.com/?enc=1T5FNqoArhjRV1lRJjdbLA%3D%3D.ICXtikay2SGDhNvFgG0xbSsAbW3fOxK1qKclxY7Rpdb%2FDNMv2q%2BiT70Tf7fpSvb1DJmPdZyz55Lgp%2BAYZpW1SQ%3D%3D" rel="nofollow">如何做到 jQuery-free?</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=NssMYkU569qlLV%2BkmN0AGg%3D%3D.PaNmWi92FSZSkfShZUlGH%2FPlIhILQWlMpJ6Dro8LqdVFKY71M1Fv%2F2qU%2FJvzCSLbfThXVFCLYv2pmpOpLq1hm873Ws0AzQ9IefOQ4CmOJzc%3D" rel="nofollow">You Don't Need jQuery!</a>-events)</p></li>
</ul>
<h2>最后</h2>
<p>惯例地来宣传一下团队开源的 React PC 组件库 <a href="https://link.segmentfault.com/?enc=EB4C989BYCszhaTbfgcXTQ%3D%3D.%2BeXgz4dP79itZ4ahs9q40mmRJ2kRWnyzOSToKObjYUM6rwJCDgw67jpFGtkY26us" rel="nofollow">UXCore</a> ,上面提到的点,在我们的组件优化过程中(如 <a href="https://link.segmentfault.com/?enc=63UbWcGl8x0Wa2CTidADAQ%3D%3D.BxzWQgHdEHVmf1BsdTnszM3cvCX4ri3cv3kHFuTnrujluLhzjteqyi%2FM379DP3DB" rel="nofollow">table</a>)都有体现,欢迎大家一起讨论,也欢迎在我们的 <a href="https://segmentfault.com/t/uxcore">SegmentFault 专题</a>下进行提问讨论。</p>
<p><img src="https://gw.alicdn.com/tps/TB1TVapKFXXXXbbXpXXXXXXXXXX-1000-500.png" alt="uxcore" title="uxcore"></p>
从零开始的 React 组件开发之路 (一):表格篇
https://segmentfault.com/a/1190000006004245
2016-07-19T11:04:11+08:00
2016-07-19T11:04:11+08:00
紅白
https://segmentfault.com/u/eternalsky
12
<h2>React 下的表格狂想曲</h2>
<h3>0. 前言</h3>
<p>欢迎大家阅读「从零开始的 React 组件开发之路」系列第一篇,表格篇。本系列的特色是从 需求分析、API 设计和代码设计 三个递进的过程中,由简到繁地开发一个 React 组件,并在讲解过程中穿插一些 React 组件开发的技巧和心得。 </p>
<p>为什么从表格开始呢?在企业系统中,表格是最常见但功能需求最丰富的组件之一,同时也是基于 React 数据驱动的思想受益最多的组件之一,十分具有代表性。这篇文章也是近期南京谷歌开发者大会前端专场的分享总结。UXCore table 组件 <a href="https://link.segmentfault.com/?enc=wNp1CwW6RXoLgH6eF79jlA%3D%3D.UOIUpx56zGyRZr6kU08W83l0C1VvqNwTPgLFObloZF3Yo2ikpczeYaL2i6XY%2Bqez" rel="nofollow">Demo</a> 也可以和本文行文思路相契合,可做参考。</p>
<p><img src="/img/remote/1460000006770470?w=907&h=552" alt="" title=""></p>
<h3>1. 一个简单 React 表格的构造</h3>
<h4>1.1 需求分析</h4>
<ul>
<li><p>有表头,每行的展示方式相同,只是数据上有所不同</p></li>
<li><p>每一列可能有不同的对齐方式,可能有不同的展示类型,比如金额,比如手机号码等</p></li>
</ul>
<h4>1.2 API 设计</h4>
<ul>
<li><p>因为每一列的展示类型不同,因此列配置应该作为一个 Prop,由于有多列应该是一个数组</p></li>
<li><p>数据源应该作为基础配置之一,应该作为一个 prop,由于有多行也应该是一个数组</p></li>
<li><p>现在的样子:<Table columns={[]} data={[]} /></p></li>
<li><p>基本思路是通过遍历列配置来生成每一行</p></li>
<li>
<p>data 中的每一个元素应该是一行的数据,是一个 hash 对象。</p>
<pre><code class="js">{
city: '北京',
name: '小李'
}</code></pre>
</li>
<li>
<p>columns 中的每一个元素是一列的配置,也是一个 hash 对象,至少应该包括如下几部分:</p>
<pre><code class="js">{
title: '表头',
dataKey: 'city', // 该列使用行中的哪个 key 进行显示
}</code></pre>
</li>
<li>
<p>易用性与通用性的平衡</p>
<ul>
<li><p>易用性与通用性互相制衡,但并不是绝对矛盾。</p></li>
<li><p>何为易用?使用尽量少的配置来完成最典型的场景。</p></li>
<li><p>何为通用?提供尽量多的定制接口已适应各种不同场景。</p></li>
<li><p>在 API 设计上尽量开放保证通用性</p></li>
<li><p>在默认值上提炼最典型的场景提高易用性。</p></li>
<li><p>从易用性角度出发</p></li>
</ul>
<pre><code class="js">{
align: 'left', // 默认左对齐
type: 'money/action', // 提供 'money', 'card', 'cnmobile' 等常用格式化形式
delimiter: ',', // 格式化时的分隔符,默认是空格
actions: { // 表格中常见的操作列,不以数据进行渲染,只包含动作,hash 对象使配置最简化
"编辑": function() {doEdit();}
},
}</code></pre>
<ul><li><p>从通用性角度出发</p></li></ul>
<pre><code class="js">{
actions: [ // 相对繁琐,但定制能力更强
{
title: '编辑',
callback: function() {doEdit();},
render: function(rowData) {
// 根据当前行数据,决定是否渲染,及渲染成定制的样子
}
}
],
render: function(cellData, rowData) {
// 根据当前行数据,完全由用户决定如何渲染
return <span>{`${rowData.city} - ${rowData.name}`}</span>
}
}</code></pre>
<ul><li>
<p>提供定制化渲染的两种方式:</p>
<ul><li><p>渲染函数 (更推荐)</p></li></ul>
<pre><code class="js">{
render: function(rowData) {
return <CustomComp url={rowData.url} />
},
}</code></pre>
<ul><li><p>渲染组件</p></li></ul>
<pre><code class="js">{
renderComp: <CustomComp />, // 内部接收 rowData 作为参数
}</code></pre>
<ul><li>
<p>推荐渲染函数的原因:</p>
<ol>
<li><p>函数在做属性比较时,更简单</p></li>
<li><p>约定更少,渲染组件的方式需要配合 <code>Table</code> 预留比如 <code>rowData</code> 一类的接口,不够灵活。</p></li>
</ol>
</li></ul>
</li></ul>
</li>
</ul>
<h4>1.3 代码设计</h4>
<p><img src="/img/remote/1460000006004249" alt="Table 分层" title="Table 分层"></p>
<blockquote><p>图:Table 的分层设计</p></blockquote>
<p><img src="/img/remote/1460000006004251" alt="table 简单架构" title="table 简单架构"></p>
<blockquote><p>图:最初的 Table 结构,详细的分层为后续的功能扩展做好准备。</p></blockquote>
<h3>2. 加入更多的内置功能</h3>
<blockquote><p>目前的表格可以满足我们的最简单常用的场景,但仍然有很多经常需要使用的功能没有支持,如列排序,分页,搜索过滤、常用动作条、行选择和行筛选等。</p></blockquote>
<h4>2.1 需求分析</h4>
<ul>
<li><p>列排序:升序/降序/默认顺序 Head/Cell 相关</p></li>
<li><p>分页:当表格需要展示的条数很多时,分页展示固定的条数 Table/Pagination 相关,这里假设已有 Pagination 组件</p></li>
<li><p>搜索过滤:Table 相关</p></li>
<li><p>常用操作:Table 相关</p></li>
<li><p>行选择:选中某些行,Row/Cell 相关</p></li>
<li><p>行筛选:手动展示或者隐藏一些行,不属于任何一列,因此是 Table 级</p></li>
</ul>
<h4>2.2 API 设计</h4>
<blockquote><p>根据上面对于功能的需求分析,我们很容易定位 API 的位置,完成相应的扩展。</p></blockquote>
<pre><code class="js">// table 配置,需求对应的模块对应了他的配置在整个配置中的位置
{
columns: [ // HEAD/ROW 相关
{
order: true, // 是否展示排序按钮
hidden: false, // 是否隐藏,行筛选需要
}
],
onOrder: function (activeColumn, order) { // 排序时的回调
doOrder(activeColumn, order)
},
actionBar: { // 常用操作条
"打印": function() {doPrint()},
},
showSeach: true, // 是否显示搜索过滤,为什么不直接用下面的,这里也是设计上的一个优化点
onSearch: function(keyword) { doSearch(keyword) }, // 搜索时的回调
showPager: true, // 是否显示分页
onPagerChange: function(current, pageSize) {}, // 分页改变时的回调
rowSelection: { // 行选择相关
onSelect: function(isSelected, currentRow, selectedRows) {
doSelect()
}
}
}
// data 结构
{
data: [{
city: 'xxx',
name: 'xxx',
__selected__: true, // 行选择相关,用以标记该行是否被选中,用前后的 __ 来做特殊标记,另一方面也尽可能避免与用户的字段重复
}],
currentPage: 1, // 当前页数
totalCount: 50, // 总条数
}</code></pre>
<h4>2.3 代码设计</h4>
<h4>结构图</h4>
<p><img src="/img/remote/1460000006004253" alt="" title=""></p>
<blockquote><p>图:扩展后的 Table 结构</p></blockquote>
<h4>内部数据的处理</h4>
<blockquote><p>目前组件的数据流向还比较简单,我们似乎可以全部通过 props 来控制状态,制作一个 stateless 的组件。</p></blockquote>
<h4>何时该用 state?何时该用 props?</h4>
<p><strong>UI=fn(state, props)</strong>, 人们常说 React 组件是一个状态机,但我们应该清楚的是他是由 state 和 props 构成的双状态机;</p>
<p>props 和 state 的改变都会触发组件的重新渲染,那么我们使用它们的时机分别是什么呢?由于 state 是组件自身维护的,并不与他的父级组件进行沟通,进而也无法与他的兄弟组件进行沟通,因此我们应该尽量只在页面的根节点组件或者复杂组件的根节点组件使用 state,而在其他情况下尽量只使用 props,这可以增强整个 React 项目的可预知性和可控性。</p>
<p>但凡事不是绝对的,全都使用 Props 固然可以使组件可维护性变强,但全部交给用户来操作会使用户的使用成本大大提高,利用 state,我们可以让组件自己维护一些状态,从而减轻用户使用的负担。</p>
<p>我们举个简单的例子</p>
<pre><code class="js">{/* 受控模式 */}
<input value="a" onChange={ function() {doChange()} } />
{/* 非受控模式 */}
<input onChange={ function() {doChange()} } /></code></pre>
<p>value 配置时,input 的值由 value 控制,value 没有配置时,input 的值由自己控制,如果把 <input /> 看做一个组件,那么此时可以认为 input 此时有一个 state 是 value。显然,无 value 状态下的配置更少,降低了使用的成本,我们在做组件时也可以参考这种模式。</p>
<p>例如在我们希望为用户提供 <code>行选择</code> 的功能时,用户通常是不希望自己去控制行的变化的,而只是关心行的变化时应该拿取的数据,此时我们就可以将 data 这个 prop 变成 state。有一点需要注意的是,用户的 prop</p>
<pre><code class="js">class Table extends React.Component {
constructor(props) {
super(props);
this.data = deepcopy(props.data);
this.state = {
data: this.data,
};
}
/**
* 在 data 发生改变时,更改对应的 state 值。
*/
componentWillReceiveProps(nextProps, nextState) {
if (!deepEqual(nextProps.data, this.data) {
this.data = deepcopy(nextProps.data);
this.setState({
data: this.data,
});
}
}
}</code></pre>
<p>这里涉及的一个很重要的点,就是如何处理一个复杂类型数据的 prop 作为 state。因为 JS 对象传地址的特性,如果我们直接对比 <code>nextProps.data</code> 和 <code>this.props.data</code> 有些情况下会永远相等(当用户直接修改 data 的情况下),所以我们需要对这个 prop 做一个备份。</p>
<h4>生命周期的使用时机</h4>
<p><img src="/img/remote/1460000007277146?w=853&h=954" alt="" title=""></p>
<blockquote><p>图:React 的生命周期</p></blockquote>
<ul>
<li><p>constructor: 尽量简洁,只做最基本的 state 初始化</p></li>
<li><p>willMount: 一些内部使用变量的初始化</p></li>
<li><p>render: 触发非常频繁,尽量只做渲染相关的事情。</p></li>
<li><p>didMount: 一些不影响初始化的操作应该在这里完成,比如根据浏览器不同进行操作,获取数据,监听 document 事件等(server render)。</p></li>
<li><p>willUnmount: 销毁操作,销毁计时器,销毁自己的事件监听等。</p></li>
<li><p>willReceiveProps: 当有 prop 做 state 时,监听 prop 的变化去改变 state,在这个生命周期里 setState 不会触发两次渲染。</p></li>
<li><p>shouldComponentUpdate: 手动判断组件是否应该更新,避免因为页面更新造成的无谓更新,组件的重要优化点之一。</p></li>
<li><p>willUpdate: 在 state 变化后如果需要修改一些变量,可以在这里执行。</p></li>
<li><p>didUpdate: 与 didMount 类似,进行一些不影响到 render 的操作,update 相关的生命周期里最好不要做 setState 操作,否则容易造成死循环。</p></li>
</ul>
<h4>父子级组件间的通信</h4>
<p>父级向子级通信不用多说,使用 prop 进行传递,那么子级向父级通信呢?有人会说,靠回调啊~ onChange等等,本质上是没有错误的,但当组件比较复杂,存在多级结构时,如果每一级都去处理他的子级的回调的话,不仅写起来非常麻烦,而且很多时候是没有意义的。</p>
<p>我们采取的办法是,只在顶级组件也就是 Table 这一层控制所有的 state,其他的各个子层都是完全由 prop 来控制,这样一来,我们只需要 Table 去操作数据,那么我们逐级向下传递一个属于 Table 的回调函数,完成所有子级都只向 Table 做“汇报”,进行跨级通信。</p>
<p><img src="/img/remote/1460000006004257" alt="" title=""></p>
<blockquote><p>图:父子级间的通信</p></blockquote>
<h3>3. 自行获取数据</h3>
<h4>3.1 需求分析</h4>
<p>作为一个尽可能为用户提高效率的组件,除了手动传入 data 外,我们也应该有自行获取数据的能力,用户只需要配置 url 和相应的参数就可以完成表格的配置,为此我们可能需要以下参数:</p>
<ul>
<li><p>数据源,返回的数据格式应和我们之前定义的 data 数据结构一致。 (易用)</p></li>
<li><p>随请求一起发出去的参数。(通用)</p></li>
<li><p>在发请求前的回调,可以在这里调整发送的参数。(通用)</p></li>
<li><p>请求回来后的回调,可以在这里调整数据结构以满足对 data 的要求。(通用)</p></li>
<li><p>同时要考虑到内置功能的适配。(易用)</p></li>
</ul>
<h4>3.2 API 设计</h4>
<pre><code class="js">// table 配置,需求对应的模块对应了他的配置在整个配置中的位置
{
url: "//fetchurl.com/data", // 数据源,只支持 json 和 jsonp
fetchParams: { // 额外的一些参数
token: "xxxabxc_sa"
},
beforeFetch: function(data, from) { // data 为要发送的参数,from 参数用来区分发起 fetch 的来源(分页,排序,搜索还是其他位置)
return data; // 返回值为真正发送的参数
},
afterFetch: function(result) { // result 为请求回来的数据
return process(result); // 返回值为真正交给 table 进行展示的数据。
},
}</code></pre>
<h4>3.3 代码设计</h4>
<blockquote><p>基于前面良好的通信模式,url 的扩展变得非常简单,只需要在所有的回调中加入是否配置 url 的判断即可。</p></blockquote>
<pre><code class="js">class Table extends React.Component {
constructor(props) {
super(props);
this.data = deepcopy(props.data);
this.fetchParams = deepcopy(props.fetchParams);
this.state = {
data: this.data,
};
}
/**
* 获取数据的方法
*/
fetchData(props, from) {
props = props || this.props;
const otherParams = process(this.state);
ajax(props.url, this.fetchParams, otherParams, from);
}
/**
* 搜索时的回调
*/
handleSearch(key) {
if (this.props.url) {
this.setState({
searchKey: key,
}, () => {
this.fetchData();
});
} else {
this.props.onSearch(key);
}
}
componentDidMount() {
if (this.props.url) {
this.fetchData();
}
}
componentWillReceiveProps(nextProps, nextState) {
let newState = {};
if (!deepEqual(nextProps.data, this.data) {
this.data = deepcopy(nextProps.data);
newState['data'] = this.data;
}
if (!deepEqual(nextProps.fetchParams, this.fetchParams)) {
this.fetchParams = deepcopy(nextProps.fetchParams);
this.fetchData();
}
if (nextProps.url !== this.props.url) {
this.fetchData(nextProps);
}
if (Object.keys(newState) !== 0) {
this.setState(newState);
}
}
}</code></pre>
<h3>4. 行内编辑</h3>
<h4>4.1 需求分析</h4>
<p>通过双击或者点击编辑按钮,实现行内可编辑状态的切换。如果只是变成普通的文本框那就太 low 了,有追求的我们希望每个列根据数据类型可以有不同的编辑形式。既然是可编辑的,那么关于表单的一套东西都适用,他要可以验证,可以重置,也可以联动。</p>
<h4>4.2 API 设计</h4>
<pre><code class="js">// table 配置,需求对应的模块对应了他的配置在整个配置中的位置,显然行内编辑是和列相关的
{
columns: [ // HEAD/ROW 相关
{
dataKey: 'cityName', // 展示时操作的变量
editKey: 'cityValue', // 编辑时操作的变量
customField: SelectField, // 编辑状态的类型
config: {}, // 编辑状态的一些配置
renderChildren: function() {
return [
{id: 'bj', name: '北京'},
{id: 'hz', name: '杭州'}].map((item) => {
return <Option key={item.id}>{item.name}</Option>
});
},
rules: function(value) { // 校验相关
return true;
}
}
],
onChange: function(result) {
doSth(result); // result 包括 {data: 表格的所有数据, changedData: 变动行的数据, dataKey: xxx, editKey: xxx, pass: 正在编辑的域是否通过校验}
}
}</code></pre>
<pre><code class="js">// data 结构
{
data: [{
cityName: 'xxx',
cityValue: 'yyy',
name: 'xxx',
__selected__: true,
__mode__: "edit", // 用来区分当前行的状态
}],
currentPage: 1, // 当前页数
totalCount: 50, // 总条数
}</code></pre>
<h4>4.3 代码设计</h4>
<p><img src="/img/remote/1460000006004259" alt="table edit mode" title="table edit mode"></p>
<blockquote><p>图:行内编辑模式下的表格架构</p></blockquote>
<ul>
<li><p>所有的 CellField 继承一个基类 Field,这个基类提供通用的与 Table 通信,校验的方式,具体的 Field 只负责交互部分的实现。</p></li>
<li><p>下面是这部分设计的具体代码实现,碍于篇幅,不在文章中直接贴出。</p></li>
<li><p><a href="https://link.segmentfault.com/?enc=bbHNcj%2BbXA6i8In%2F4tYhmg%3D%3D.NgjsQWVNX87aDaXizRXfxEfFl7W7KP4%2FJOa8Jt7FISKKhGyHPmFoVddJAPb1EcWFmvd1fAp%2FrzGdjNmBodUlHanMNIcLMVZYl5G7NKNrKHk%3D" rel="nofollow">https://github.com/uxcore/uxc...</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=LZtc0tUGslyvDqUfLPx3rw%3D%3D.kxCk4RvmNZS9gJT7PpbSM4ijpmXA3q%2BsC1l0ZQ6HSbSBOj70f4QG5MQY9fEIfr0YuTM7Rca6YJcyMznyxhGvexrSZ1sBLPI%2BHmsBNO0MP58%3D" rel="nofollow">https://github.com/uxcore/uxc...</a></p></li>
</ul>
<h3>5. 总结</h3>
<blockquote><p>这篇文章以复杂表格组件的开发为切入点,讨论了以下内容:</p></blockquote>
<ul>
<li><p>组件设计的通用流程</p></li>
<li><p>组件分层架构与 API 的对应设计</p></li>
<li><p>组件设计中易用性与通用性的权衡</p></li>
<li><p>State 和 Props 的正确使用</p></li>
<li><p>生命周期的实战应用</p></li>
<li><p>父子级间组件通信</p></li>
</ul>
<blockquote><p>碍于整体篇幅,有一些和这个组件相关的点未详细讨论,我们会在本系列的后续文章中详细说明。</p></blockquote>
<ul>
<li><p>数据的 不可变性(immutability)</p></li>
<li><p>shouldComponentUpdate 和 pure render</p></li>
<li><p>树形表格 和 数据的递归处理</p></li>
<li><p>在目前架构上进行折叠面板的扩展</p></li>
</ul>
<h3>最后</h3>
<p>惯例地来宣传一下团队开源的 React PC 组件库 <a href="https://link.segmentfault.com/?enc=Zu37qkDqHHP82%2Fid7hj2Zw%3D%3D.6JZHubAEOaswc2uKY3Qf0JB4wZVHeQgN8ra2K6EYuSoTmQholjIkFGeCRvoB%2B5nB" rel="nofollow">UXCore</a> ,上面提到的点,在我们的<a href="https://link.segmentfault.com/?enc=v%2FrPjHdENxLiRR7UVwOICg%3D%3D.bU2xPI2K%2FJ5cWOFGZyLLa92izAmKkPLJ17WfnH06eCUXaTAnj3KkerkbQ456XxFc" rel="nofollow">组件开发工具</a>中都有体现,欢迎大家一起讨论,也欢迎在我们的 <a href="https://segmentfault.com/t/uxcore">SegmentFault 专题</a>下进行提问讨论。</p>
<p><img src="/img/remote/1460000006768158?w=1000&h=500" alt="uxcore" title="uxcore"></p>
一个靠谱的前端开源项目需要什么?
https://segmentfault.com/a/1190000005859766
2016-07-03T00:21:16+08:00
2016-07-03T00:21:16+08:00
紅白
https://segmentfault.com/u/eternalsky
13
<h2>0. 前言</h2>
<p>写前端代码一段时间之后,你可能会萌生做一个开源项目的想法,一方面将自己的好点子分享出去让更多的人受益,另一方面也可以在社区贡献的环境下学到更多的东西从而快速成长。但是开源项目也有开源项目的玩法,一些可能没有注意的点,也许会让你的好点子和许多人失之交臂,在这里笔者以自身经验出发,聊一聊笔者心目中的靠谱的 Github 前端开源项目应该具有什么。当然我们讨论的只是一个项目至少需要什么才是靠谱的。真的想要做好一个项目,需要的比这里要讲的多得多。</p>
<blockquote>限于篇幅,本文很多点都是点到为止,文章中有大量链接供同学进一步了解掌握相关知识。</blockquote>
<h2>1. 文档</h2>
<blockquote>文档是你的潜在用户了解项目的第一道窗口,文档书写的好坏直接决定了项目的易用性和用户粘性。</blockquote>
<h3>1.1 README</h3>
<p>我们首先要提的是 README 文档,这是每个开源项目所必须的,README 文档会默认展示在项目首页,可以算作是整个项目的门面。一个靠谱的 README 应该包含以下几部分:</p>
<ul>
<li>言简意赅的项目介绍:你的项目解决了什么核心问题,有哪些令人心动的特性。</li>
<li>简单的安装和使用指导:用户如何安装你的项目,又如何使用在自己的项目中,你应该想办法让这部分尽量简单,降低接受成本。</li>
<li>API 和配置说明:也许你的项目十分强大,支持很多特性,你需要告诉用户如何通过配置和 API 来完成这些事情,在这中间,也许你可以插入一些简单的 demo,帮助用户理解,这部分的好坏直接决定了项目的易用性。</li>
</ul>
<blockquote>随着你的项目日趋复杂,也许 README 的篇幅已经不能够承载你的 API 说明,这时在项目中单独建立一个 doc 文件夹用来存放文档,也是很多人的选择。</blockquote>
<ul>
<li>如何贡献:开源的一个很重要的目的是为了让社区中的更多人来帮助你完善你的创意,你需要告诉他们你有哪些部分是他们可以帮的上忙的,你是如何对待 issues 和 pull requests( 后文称 pr) 的。</li>
<li>如何与作者取得联系:有些合作和洽谈可能无法承载在 issue 上,告诉用户除了 issues,还有什么途径可以与作者取得联系。</li>
<li>开源许可协议:既然是一个开源项目,那么开源许可协议必然是不可少的,你的开源许可是 MIT、BSD 还是 ISC、Apache?当然你也需要了解这些开源许可的意义,这里推荐一篇<a href="https://link.segmentfault.com/?enc=r8nGkqaQhQznOm5FZwuGhQ%3D%3D.Eorqu0UktnGE%2FkWiDUoSdBfO9seSoM44py7v6WFWj4ZEaSCA6Qha9V9MEh9iX1OO" rel="nofollow">知乎问答</a>。</li>
</ul>
<h3>1.2 CONTRIBUTING</h3>
<p>上面我们也提到了如何贡献的问题,当贡献需要了解的东西越来越多的时候,我们也习惯于把它单独抽成一份 CONTRIBUTING.md。在贡献文档中应该详细地告诉贡献者,哪些功能点是需要贡献者参与的,如何提 issue 以及 pr。</p>
<h3>1.3 LICENSE</h3>
<p>除了在 README 中提到遵循的开源协议外,一个靠谱的开源项目还会将该开源协议的内容文档放在自己的项目下方。</p>
<h2>2. 代码风格</h2>
<blockquote>关于代码风格,每个人可能都会有自己的偏好,这些偏好中有好的,也有坏的,一些不好的代码风格会让其他开发者感到不快,减少了大家对于代码的关注,好在强大的开源社区中同样也有人开源了代码风格规范,这些代码规范都经过了激烈的讨论和充分的修改,为大多数人所认可,需要注意的是代码风格并不只是缩进、空格这一类的事情,它更多地是一种代码习惯,比如何时使用 <code>const</code>,何时使用 <code>let</code>,是否有声明但未使用的变量等等,这些习惯并不影响代码的功能,却可以很大程度上决定代码的可维护性、易读性和降低犯错的机会,同时也让其他开发者对你的项目刮目相看。</blockquote>
<h3>2.1 代码风格推荐</h3>
<h5>2.1.1 Airbnb: <a href="https://link.segmentfault.com/?enc=U7wDuv8BUFJZw1vjrIhDIA%3D%3D.sZTt3n0fTI44FCZQxtnu7Baz8C6e%2BiWDmpQzspKKMO%2BW0205GkUTAcGKhRS%2Fo011" rel="nofollow">https://github.com/airbnb/jav...</a>
</h5>
<p>Airbnb 的 js 规范应该是目前受认可度最高的代码规范之一,目前在 Github 上已经累加了 36700+ 颗星,包含的领域非常广泛,包括线下时髦的 ES6 和 React JSX 等等,笔者推荐。</p>
<h5>2.1.2 idiomatic.js: <a href="https://link.segmentfault.com/?enc=1qPdXAFNCuEsU761J8DZ0Q%3D%3D.WW3nykeuFl3NHQ3b0gkvORth%2BICsGLHqIkSs4fSEoE0OTW%2FSwfGtuDCPpdFkHC3P" rel="nofollow">https://github.com/rwaldron/i...</a>
</h5>
<p>这是一份有着很长历史的,由一群经验丰富的 js 工程师维护的代码风格规范,同时也十分通俗易懂,另外他也有简体中文的版本。</p>
<h5>2.1.3 jQuery:<a href="https://link.segmentfault.com/?enc=IQoPsW8itY%2BK%2FBzSTGKdjw%3D%3D.MX%2B6Bx8TN%2BqV4pmwKBmZYiXtHGw6I1jF7Cgv%2FQjnRCZ0LKAwA%2BsKS3eY5RpWkBb8" rel="nofollow">https://contribute.jquery.org...</a>
</h5>
<p>jQuery 所倡导和使用的代码规范,大量的 jQuery 组件根据这一规范书写,如果你读过 jQuery 的源码,你喜欢他的风格,或者你正在开发一款 jQuery 的插件,那这也是一个不错的选择。</p>
<h5>2.1.4 Google:<a href="https://link.segmentfault.com/?enc=OPVx5WmJOgE3zLTs8n9n4g%3D%3D.HfRG9iTmyzfDo2iEDAY4uyh61Ic2wAOR98bvfXpPhNGZJAwScycPBTTus1W3OMrCm9NA9js06OU7ow98b9UGxA%3D%3D" rel="nofollow">https://google.github.io/styl...</a>
</h5>
<p>谷歌倡导的代码风格,自 2012 年推出以后已经有很多谷歌的项目遵循这份规范进行编码,喜欢谷歌风格的朋友可以使用。</p>
<h3>2.2 LINT</h3>
<blockquote>看到这里有人会有疑问,规范虽然好,可是条目太多了,我虽然可以学习,但是全都记住难度太高了。不用担心,你的痛点也是大家的痛点,早已经有工具帮助我们来解决这一问题,而且更棒的是他可以无缝地与你的 IDE 相结合。</blockquote>
<p>在这里我们要推荐 <a href="https://link.segmentfault.com/?enc=vfClhmPJjVN8Zx6GM%2Fz%2FJQ%3D%3D.bTWU9FuN802F5LGAkNVO63iz6tH5EjXzBacOgKV90KQ%3D" rel="nofollow">ESLint</a> 这款工具。在不久之前,你还有另一个选择 <a href="https://link.segmentfault.com/?enc=MpXhFNhOm%2F57KyhbAp1S%2Fw%3D%3D.hFqvL3kpZq7Oi5NalXtDLqngPh%2FKj%2B3cNFQa55YrybY%3D" rel="nofollow">JSCS</a>,但在最近,JSCS 团队选择与 ESLint 团队进行合并,专注于一款产品 <a href="https://link.segmentfault.com/?enc=dfuyeoR9mWZYLUdNwsKxtA%3D%3D.YcW6XYd7UgJ6DQunPGm%2FU%2Bpu3yUYzjc1b5Ttze6KJSM%3D" rel="nofollow">ESLint</a> 的开发,两大大牛团队的合体想必会带给 <a href="https://link.segmentfault.com/?enc=HixtuLo%2Bf3c8LZ3IwWFPvQ%3D%3D.XaRepiqcKTjc3BQzeUE3TfbhviQlYpWz2Q5MGS55JJ4%3D" rel="nofollow">ESLint</a> 更为强大的生命。</p>
<p><img src="/img/remote/1460000006768157?w=524&h=235" alt="eslint & jscs" title="eslint & jscs"><br>图1:JSCS 与 ESLint 已经合并</p>
<p>ESlint 提供了非常丰富的 IDE 集成工具,目前已经支持 Sublime Text 3, Vim, Emacs, Eclipse, TextMate 2, Atom, IDEA 和 Visual Studio Code。具体的插件工具请移步 ESlint 的<a href="https://link.segmentfault.com/?enc=hsg3XTY3eFqRSWaQX4gUjw%3D%3D.%2FeInLJduefxR1EgGd5XnrGfX8WKMj5gJH9yaPOefMnbZx8TmCmVN1Iz5HqNHXrcv" rel="nofollow">集成文档</a>。下面我们以 sublime 为例,简单讲一下如何使用这些插件:</p>
<ul>
<li>
<p>首先全局安装 <code>ESLint</code>:</p>
<pre><code class="sh">npm install -g eslint</code></pre>
</li>
<li>接着通过 Package Control,安装 <a href="https://link.segmentfault.com/?enc=QxZ6cgP3mhtrQhsWmIyoZg%3D%3D.SZrZBM8iXYEMGhw9H777T6FCNc2th%2B2w1phW%2FdAhlNNJuzsxqOR0PklP8UQfJhfk" rel="nofollow">SublimeLinter</a> 和 <a href="https://link.segmentfault.com/?enc=e4XkxsM15JTj3tWZjFaJFQ%3D%3D.nODPJiObngNOcIJKfzvmO%2FB%2BuZ4F9Za%2BKcypYw73%2F2LE9yC4gBlLjAGv1C4BcknuchUL8bwluLkU5m8bKicxxg%3D%3D" rel="nofollow">SublimeLinter-contrib-eslint</a>。</li>
<li>
<p>初始化 eslint 配置</p>
<pre><code class="sh">eslint --init</code></pre>
<p>这里会有一些人机交互,来选择 <code>eslint</code> 的风格,之后 <code>eslint</code> 就会在你的项目下添加对应的依赖,重启 sublime,就可以看到效果了。</p>
</li>
</ul>
<p><img src="/img/remote/1460000005859769" alt="lint 演示" title="lint 演示"><br>图2:eslint sublime 插件演示</p>
<p>我们可以看到,linter 对于不符合规则的行和代码块会标红,将鼠标点击到对应标红的块,会显示报错的原因,比如上图中是因为 (global-require) 规则。有时仅通过提示的错误文案,无法帮你准确理解含义,这时我们还可以借助 eslint 的站点:<a href="https://link.segmentfault.com/?enc=YZJm16h%2BSXHsp5kze8Uj6g%3D%3D.jt%2Bk%2Badw8MqjHxPP1m8kbpkMfEIEl%2Bw6g5jMIxvDfE4%3D" rel="nofollow">http://eslint.org/docs/rules/</a> ,这里有更详细的讲解,你还可以直接搜索对应的规则。</p>
<h2>3. 测试</h2>
<blockquote>靠人工来保证项目的维护总是不出差错是不靠谱的,人总有健忘和糊涂的时候,尤其是当项目越来越复杂时,一个人甚至可能无法全部了解项目的全部逻辑,这时我们就要依靠测试来保证项目的可靠性,这里的测试包括但不限于,单元功能测试,UI 测试,兼容性测试等等。</blockquote>
<h3>3.1 测试体系</h3>
<p>一个测试体系大体应该包含以下三部分,这三者功能上互相独立,但合作完成一次测试:</p>
<ul>
<li>测试运行器(Test runner):主要负责测试的自动化,主要负责调用测试框架和测试代码,自动打开浏览器或者 js 环境,执行测试。</li>
<li>测试框架(Testing Framework):测试框架规定了测试风格和测试的生命周期(lifeCircle hook)。</li>
<li>断言库(Assertion library):每个测试单元是通过一句或者一组断言来组成的,每一句断言声明了对一种功能的描述。例如 <code>expect(window.r).to.be(undefined);</code>。</li>
</ul>
<h3>3.2 Test runner</h3>
<p>一个优秀的 runner 可以做很多事情,我们以 Google Angular 团队推出的 <a href="https://link.segmentfault.com/?enc=KcqXFZXBPOg%2F9Npi9N%2F2yA%3D%3D.K8AJKvcAQ2uNVIrHO7tCne%2BBFwkmNNVGhD5r4ivzMLVxRUVCZ2eJi2%2Bqju57Kb%2FS" rel="nofollow">karma</a> 为例,你可以指定在什么浏览器中,使用什么测试框架,进一步地完成测试框架的配置,以及其他<a href="https://link.segmentfault.com/?enc=DTZHGIz1IkfJO1nhLU4yCw%3D%3D.9eGu1%2BEfvtkY9%2BqxHHXHzFZcKH4jrfpGmeIsauCb09632sGCy0uFg9ahm7AlvLGTEU5EUOPfi%2BdzmM6eZnkgaWxrPFtwHazhpPjw1tM7rpA%3D" rel="nofollow">非常多的定制</a>。他的安装和初始化也非常简单:</p>
<pre><code class="sh"># Install Karma:
$ npm install karma --save-dev
# Install plugins that your project needs:
$ npm install karma-jasmine karma-chrome-launcher --save-dev</code></pre>
<p>初始化配置文件</p>
<pre><code class="sh">$ karma init my.conf.js
Which testing framework do you want to use?
Press tab to list possible options. Enter to move to the next question.
> jasmine
...
Do you want Karma to watch all the files and run the tests on change?
Press tab to list possible options.
> yes
Config file generated at "/Users/vojta/Code/karma/my.conf.js".</code></pre>
<p>然后就可以运行了</p>
<pre><code># Run Karma:
$ ./node_modules/karma/bin/karma start</code></pre>
<p>当然这只是最初始化的配置,我们还没有写真正的测试,所以这里只是一个空架子。</p>
<h3>3.3 测试框架</h3>
<p>测试框架有很多:</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=lCnFiFid2XkLsNEFpzGF3Q%3D%3D.vDCCnKvtPlRfQsCADumeb%2BjCWmbbqrkPYYg5aU%2B0%2FV4%3D" rel="nofollow">mocha</a></li>
<li><a href="https://link.segmentfault.com/?enc=PG%2FMWH31cGZan%2BdVIEZPrQ%3D%3D.1mc3fsoA8WaGZ2ZteKeVVhT9zOyHTJZnZmySSfGa6vc%3D" rel="nofollow">jasmine</a></li>
<li><a href="https://link.segmentfault.com/?enc=aRwU2EmuhUF07WXo0t4v%2FA%3D%3D.Dq9PqyGf8qQAno1LSm8rcQZeZ6H1RkK%2FVV9bOu9POS8%3D" rel="nofollow">qunit</a></li>
<li><a href="https://link.segmentfault.com/?enc=m8Q12SfJyFFvbJ3Xd0CGlg%3D%3D.S4BqnsIsvafG64Hne4WE38t92RxEstLwKwkqFcStz7xuxbTgAdjy5zrGwz7NMa45" rel="nofollow">Jest</a></li>
</ul>
<p>对于一般的开发者来说,他们都能够满足日常的测试需求,所以主要看你的使用习惯,这里以 mocha 为例来给大家一个大概的印象。</p>
<p>首先安装 mocha</p>
<pre><code class="sh">$ npm install -g mocha
$ mkdir test
$ $EDITOR test/test.js</code></pre>
<p>接下来写你的 test</p>
<pre><code class="js">var assert = require('chai').assert;
describe('Array', function() {
describe('#indexOf()', function () {
it('should return -1 when the value is not present', function () {
assert.equal(-1, [1,2,3].indexOf(5));
assert.equal(-1, [1,2,3].indexOf(0));
});
});
});</code></pre>
<p>运行 test,查看结果</p>
<pre><code class="sh">mocha test/test.js</code></pre>
<p>当然上面这个测试只能测试 node 下的 js 代码,如果你的代码是前端项目,那么就要借助 phantom/browser 等工具了,同时操作这么多东西很麻烦,这时 karma 的作用的就体现出来了,本文只是大体的介绍,因此不再铺开去讲,详细请查看 <a href="https://link.segmentfault.com/?enc=dpWm5f9hG7duCUqzbql6Gw%3D%3D.hRWEHehW8V2uo6OPDhiNlHDr5JVE0agEIeeHrJNaw%2FhPARliXeVPExsunu2%2BX926" rel="nofollow">karma</a> 的官方文档。</p>
<h3>3.4 断言库</h3>
<p>断言库也有很多的选择,其中比较有名气的有:</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=NjEHg6rQQr%2FMx5zr3vZSag%3D%3D.QhyjNYsBrtAgZxJjYECLeUSb9tTcgaIEYRN0RKrRXUdC19RcCpUbWSTzM59VSLnV" rel="nofollow">expect.js</a></li>
<li><a href="https://link.segmentfault.com/?enc=Y1eUa1rDL6OlIdZI9g%2BTTw%3D%3D.ve0bCfOEAklv1VyzQ0rFiQrQ7hKBMXKm5fXRjRfOOHlB0Br9czsrA2%2F2lLgAhQ0v" rel="nofollow">should</a></li>
<li><a href="https://link.segmentfault.com/?enc=2bu5J5%2Fpc13OFTeHffCyGg%3D%3D.J%2B3syFHUgv3QT57UQEcDrkicfE9ahfVUkTo4i1n6Q9XUh74u2B6XsLlwojWBy2n3" rel="nofollow">chai</a></li>
</ul>
<p>其中,chai 同时支持 BDD 和 TDD 两种测试模式,而 expect.js 具有 IE6+ 级别的浏览器兼容性。</p>
<h5>3.4.1 测试模式</h5>
<p>有人会问 BDD 和 TDD 都代表了什么?</p>
<p>TDD</p>
<blockquote>TDD(Test-driven development),即 测试驱动开发。即先根据需求写测试,然后再写代码,使代码逐渐符合测试要求,不符合时重构代码满足。这种开发模式适合一些需求非常清晰明确,且不再变更的情况,在一般的开发中,我们还是 BDD 的模式使用的比较多。chai 提供的 assert 部分是经典的 TDD 风格,大致如下</blockquote>
<pre><code class="js">var assert = require('chai').assert
, foo = 'bar'
, beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };
assert.typeOf(foo, 'string'); // without optional message
assert.typeOf(foo, 'string', 'foo is a string'); // with optional message
assert.equal(foo, 'bar', 'foo equal `bar`');
assert.lengthOf(foo, 3, 'foo`s value has a length of 3');
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');</code></pre>
<p>BDD</p>
<blockquote>BDD(Behavior-Driven development),即 行为驱动开发。</blockquote>
<p>通常 BD测试提供了几个方法:</p>
<ul>
<li>describe()</li>
<li>it()</li>
<li>before()</li>
<li>after()</li>
<li>beforeEach()</li>
<li>afterEach()</li>
</ul>
<p>通过这些方法描述一种行为,当测试的表现满足行为的预期时,即表示测试通过。</p>
<h2>4. 持续集成</h2>
<blockquote>持续集成,continuous integration (简称 ci),指的是频繁地(一天多次)将代码集成到主干。</blockquote>
<p>为了保证这种快速迭代的开发方式不出差错,采取的核心措施:代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。这种行为虽然不能消除 bug,但有效地帮助我们即时发现错误并改正。关于持续集成的详细流程,大家参考阮老师的 <a href="https://link.segmentfault.com/?enc=5h5ELj%2Fa5%2F5MO2niKosG1w%3D%3D.4hUClJ7AT4KvvnJhu5vOeGPaGG0hCzo5KCzCzSu6cUPic9J469e%2B2AZfEhF%2FB2vXjOCHIHb%2Bm4NEQett9sBAzmJUEyDNaP4e0x27d%2Ba8E7g%3D" rel="nofollow">持续集成是什么?</a>。本文则重点介绍已经介入 github,可以帮助到我们的继承工具。</p>
<p>接入到 github 的所有 CI 可以在 <a href="https://link.segmentfault.com/?enc=nFnuB%2BH13K4cz5rbaanEig%3D%3D.leX1XWHhlZR%2BOfEgr96TVGoVPkF3EY%2F79u4xkk9%2FvVQ%3D" rel="nofollow">https://github.com/integrations</a> 中查看。</p>
<h3>4.1 CI: <a href="https://link.segmentfault.com/?enc=254aJAkXNYx%2BNuxnZ%2Fg0qw%3D%3D.3SA7Bh%2BlRvb1pLdJqE6q3Lg8rh4yaGzS0oQb7EDjFvXKKa%2FPaU%2BJ5IO5olluIBMc" rel="nofollow">Travis CI</a>/ <a href="https://link.segmentfault.com/?enc=bfXw8tgrh8gzJ%2FFKVWAhrA%3D%3D.LVjSDlFk2VBseFMZKXqUC8k7aJ1QcqZ5B6PFw2gv%2FkNuqwPIFkg7J9pDT763w3wN" rel="nofollow">CircleCI</a>
</h3>
<p>两者都是接入 Github 的持续集成工具,其核心是通过一个脚本,在代码 commit 的时候自动运行继承脚本,完成测试、构建等任务。以 Travis CI 为例,首先在 github 的 Travis CI 页面将 Travis CI 的 hook 加入到你的项目中。</p>
<p><img src="/img/remote/1460000005859771" alt="travis add project" title="travis add project"><br>图3:将 Travis CI/CircleCI 集成到你的项目中。</p>
<p>接着在你的项目目录下建立一个 <code>.travis.yml</code>,大致长成这个样子:</p>
<pre><code>language: node_js
sudo: false
notification:
email:
- xxx@hotmail.com
node_js:
- 4.0.0
env:
matrix:
- TEST_TYPE=test
- TEST_TYPE=coverage
- TEST_TYPE=saucelabs
</code></pre>
<p>在不指定 <code>.travis.yml</code> 的情况下,travis 会自动执行 <code>npm install</code> 和 <code>npm test</code> 来进行集成测试。更多的配置可以参考官方的<a href="https://link.segmentfault.com/?enc=G07y9NV%2BP%2BoyhnB6LH4hMg%3D%3D.GDWKIlFne0Ppoi5Rzx2a84CCPOWeLkZGITLMcp06AUUSVIBK6hh001S7khM%2BiauOkWSBdW3oAlSNS9G2%2BTRj0H89ClV3O1SDgvHL%2BCYT4DI%3D" rel="nofollow">文档</a>。集成通过在两个地方可以有所体现:</p>
<ul><li>一个是 ci 提供的 badge:<a href="https://link.segmentfault.com/?enc=ttBoLf5fU%2B83KMEoFbVVvA%3D%3D.XXQdroeg8fSLmCER0Jg8fK%2F7Fjoowkv0TKvVnpxwv3a9KHOZST7qKnyOj0OX9YsT" rel="nofollow"><img src="/img/remote/1460000005859773" alt="build status" title="build status"></a>
</li></ul>
<ul><li>一个是在 pr 提交时的自动 comment</li></ul>
<h3>4.2 跨浏览器集成测试:<a href="https://link.segmentfault.com/?enc=eCFKn10T%2Fk0X5V%2BZdyvpEw%3D%3D.QwZGJGGu42pF2AGdCmY0rJc5v%2BCRUnXQHXmH9Lt5HG4%3D" rel="nofollow">SAUCELABS</a> & <a href="https://link.segmentfault.com/?enc=HxTWJF2jVSFbmhmka8a0Pg%3D%3D.25TZdGeLtJzsQvfhhpY0qwigBenSPbF82ZAxnmga5OM%3D" rel="nofollow">Browser Stack</a>
</h3>
<p>这两个工具都是提供了多种浏览器环境,包括 pc 端和移动端,然后在多种环境下都是去运行测试脚本,来测试项目的浏览器兼容性。其中 <a href="https://link.segmentfault.com/?enc=vAdAsf8LmM4zx1yNvnonJw%3D%3D.GMEQVeSpCRzRve9pa%2B5JmkAot%2BFf6wnKu%2BMQXNlrBP8%3D" rel="nofollow">SAUCELABS</a> 对于开源项目完全免费,只需要走他的 Open Source Plan 即可,而 <a href="https://link.segmentfault.com/?enc=c4SEt9JOdcZTliLxuF0c1g%3D%3D.jQ%2Bvkg8tp2wxwoqbe2puMm88vXvomZQBZ%2F6q3s%2Bh53Y%3D" rel="nofollow">Browser Stack</a> 虽然也提供了 Open Source 的免费计划,但比较麻烦,需要邮件联系,并且在项目 README 中提到其对项目的支持。手动配置这些集成还是比较麻烦的,一般我们都借助 karma 来集成,使用 karma 的 <a href="https://link.segmentfault.com/?enc=YXmF7rsR2%2BTVzxszhkcoZw%3D%3D.%2B0qOInQp3hrLVC5slYXIFHYGt2252oZ15oSbH1ApUacA2icSVndR%2F8zFEhW4PtcwYB%2BJ4Q2kjh2RRhZxHweZsA%3D%3D" rel="nofollow">karma-saucelabs-launcher</a> 和 <a href="https://link.segmentfault.com/?enc=iV7oIfltkyTzEYAbjF2AQQ%3D%3D.MSLkhUaPAGmFHAHeB7jLdM%2BzubXWbNtlMPx9fOsyIRclilAwFzXhXM2C7DPaOoPojSRZAwfq%2Bvn9Nmn7EJuR7Q%3D%3D" rel="nofollow">karma-browserstack-launcher</a>。</p>
<p>saucelabs 也提供了很棒的 badge</p>
<p><a href="https://link.segmentfault.com/?enc=7Vsya0okb2uPnHzmjbfWrw%3D%3D.%2BBfDX4cnsKZHCB%2Bo3bWMu6gQ7gITmDXwkULTHhkSq%2F2TvHnl8EldXMbKLrJpFLH2" rel="nofollow"><img src="/img/remote/1460000005859774" alt="Sauce Test Status" title="Sauce Test Status"></a></p>
<h3>4.3 代码覆盖率集成 <a href="https://link.segmentfault.com/?enc=NnZ18Po%2BsGyisIhBDlKo7w%3D%3D.rDfSZilPs4Egiq0l4gGfHmnjGlsn%2FYiAUZ49%2BvRG3Ocy%2BLdklkzpJjjTszj9Xmcc" rel="nofollow">Coveralls</a>
</h3>
<p>代码覆盖率可以在本地测试里集成,同样也可以在 CI 中集成,通过引入 Coveralls,他可以将你持续集成测试中得到的代码覆盖率结果做一个记录和保存,同时及时的反馈给用户。</p>
<p>Coveralls 也有两个地方可以体现:</p>
<ul><li>一个是 Coveralls 提供的 badge:<a href="https://link.segmentfault.com/?enc=uvhrRCFgZmOUAlak8QMHjQ%3D%3D.YfKYDDqvufyA9IZVmeMPTgUm3AOq16AWxuPGoxCBpoudosT3UKeW8tMyMWwqhnfmaeAha%2Fb4MegElAmBw%2FNOQw%3D%3D" rel="nofollow"><img src="/img/remote/1460000005859776" alt="Test Coverage" title="Test Coverage"></a>
</li></ul>
<ul><li>一个是在 pr 提交时的自动 comment</li></ul>
<p><img src="/img/remote/1460000005859779" alt="coveralls" title="coveralls"><br>图4:coveralls 的自动 comment</p>
<h2>5. 总结</h2>
<p>碍于篇幅有限和行文的目的,文中提供的很多点,只是举了一些例子,点到为止,同时提供了链接,供大家日后仔细研究。作者希望通过此文,让读者了解到一个靠谱的前端开源项目应该具备的东西,这个靠谱不仅是为了让用户用的放心,更是为了开发者在开发时心中有谱。那具备了这些就代表了一个成功的开源项目吗?很遗憾,这只是通往成功的必备条件,一个成功的开源项目还需要开发者更多的经营,最重要的,是一份为用户解决痛点的初衷和持之以恒的决心。</p>
<h2>最后</h2>
<p>惯例地来宣传一下团队开源的 React PC 组件库 <a href="https://link.segmentfault.com/?enc=iJuFEmAbORCTxCmH319vnA%3D%3D.WqGGwNojZEkv2DHkwvullv5AMKdAZaMM3WmxjaEfxtJcugApyEqYSG1HlE9wMhqF" rel="nofollow">UXCore</a> ,上面提到的点,在我们的<a href="https://link.segmentfault.com/?enc=iebbTwPMhiFbV93lIy0vFg%3D%3D.38%2Fizhrw5sVKh34GNtiIB1e6HZwMpuB9%2FlAlUkMmr9rHydaJIyWmH70XzMEiE4ly" rel="nofollow">组件开发工具</a>中都有体现,欢迎大家一起讨论,也欢迎在我们的 <a href="https://segmentfault.com/t/uxcore">SegmentFault 专题</a>下进行提问讨论。</p>
<p><img src="/img/remote/1460000006768158?w=1000&h=500" alt="uxcore" title="uxcore"></p>
<blockquote>本文作者 <a href="https://link.segmentfault.com/?enc=wgryaPV%2F1i4kMFKdWBVzCA%3D%3D.cIsVSFdgcCcTGge%2FQijShUw%2BBd0YMjsooGu5rfj%2F3%2Bc%3D" rel="nofollow">eternalsky</a>,始发于团队微信公众号 <strong>猿猿相抱</strong> 和 segmentFault 专栏 <a href="https://segmentfault.com/blog/uxcore"><strong>UXCore</strong></a>,转载请保留作者信息。</blockquote>
UXCore:一个兼容主流浏览器的 React PC 组件库
https://segmentfault.com/a/1190000005805135
2016-06-26T22:49:05+08:00
2016-06-26T22:49:05+08:00
紅白
https://segmentfault.com/u/eternalsky
7
<p><img src="https://gw.alicdn.com/tps/TB1TVapKFXXXXbbXpXXXXXXXXXX-1000-500.png" alt="图1 uxcore 概念图" title="图1 uxcore 概念图"></p>
<h2>0. 为什么我们需要 UXCore?</h2>
<p>UXCore 是一个基于 React 的 PC UI 套件库,兼容 IE8+。<a href="https://link.segmentfault.com/?enc=3pRZ0nFK6kOzFZaMgNgEfQ%3D%3D.tThRn4tHoufnX6FVPlGpoQ%3D%3D" rel="nofollow">http://uxco.re/</a> <br>阿里巴巴信息平台是负责整个阿里巴巴集团智能办公系统的团队,涉及非常多的企业业务系统,包括薪酬、人力、财务、行政、IT 等等,在这些系统中产生了大量的表格、表单和图表的交互场景,这里面有很多重复配置的地方,也有很多定制变化的地方,目前业界的这一方面还没有能够完全满足这一方面的解决方案,因此有了 UXCore。 <br>UXCore 要解决的核心问题,就是方便高效地产出表单、表格,同时提供足够强大的定制能力,使用户可以对组件的每一个渲染部分进行修改,从而满足各种不同种类的业务场景。 <br>为了实现核心的目标,我们和 UED 团队积极合作,充分收集业务场景和进行视觉优化,在这个过程我们产出了一系列的简单易用的基础组件,用于构建页面的其他部分。目前信息平台的新系统都在使用 UXCore 进行承载,我们也很愿意将我们已经成熟的解决方案分享出来,帮助更多的企业系统开发者解决他们开发上的痛苦,同时也寄希望于依靠社区的反馈,可以让 UXCore 走的更好。</p>
<h2>1. UXCore 有哪些特性。</h2>
<h3>1.1 丰富的组件</h3>
<p>超过 35 个常用基础组件用来构建你的系统业务,涵盖大部分常用功能</p>
<h3>1.2 专注于企业应用</h3>
<p><img src="https://gw.alicdn.com/tps/TB1WB1bKFXXXXa2XVXXXXXXXXXX-1308-796.png" alt="图2 table 全家福/form 全家福" title="图2 table 全家福/form 全家福"></p>
<blockquote><p>图2:UXCore 致力于产出方便易用、功能强大且高度可定制的表单、表格、布局组件。'</p></blockquote>
<ul>
<li><p>表单内置 10 余种常用表单域可以直接使用。</p></li>
<li><p>表格除了基础功能外,还支持折叠展开二级面板、树形结构、行内编辑等复杂场景。</p></li>
<li><p>布局支持传统的左右自适应布局和栅格布局,轻松搞定页面排版。</p></li>
<li><p>除了内置的功能外,表单、表格还有一套简易地定制体系,随时可以在业务中为组件注入更强大的力量。</p></li>
</ul>
<h3>1.3 全面的国际化支持</h3>
<p>所有组件的内置文案支持国际化,大部分文案用户可以主动设置。</p>
<h3>1.4 可以定制的主题</h3>
<p><img src="https://gw.alicdn.com/tps/TB1N8mdKFXXXXasXVXXXXXXXXXX-1170-583.png" alt="图3 定制主题" title="图3 定制主题"></p>
<blockquote><p>我们默认提供了两套主题供使用,这两套主题也同时在我们的系统中使用,如果你不喜欢我们的主题,你也可以使用我们的<a href="https://link.segmentfault.com/?enc=S8m7Frdc5CqDhi4TMBIzww%3D%3D.FAxtE0MI0ey1OvYKGT2JgM3C2%2F9XDtMMYxJhBqSK7nI%3D" rel="nofollow">在线定制工具</a>定制你的主题,我们目前开放了所有颜色的定制,未来可能会开放更多的定制点。</p></blockquote>
<h3>1.5 按需使用</h3>
<p>UXCore 的每个组件都是单独项目维护的,并且也会单独发布于 npm,因此如果你不喜欢引用 UXCore 的大全包,你也可以单独引用独立的组件使用。同时,修改 UXCore 原有的样式也十分简单,只需修改类名前缀(prefixCls),即可定制属于自己的 UXCore 组件风格。</p>
<h3>1.6 提供项目级的建议和支持</h3>
<p>如果你苦于搭建使用 React 和 UXCore 的项目环境,可以参考我们的 <a href="https://link.segmentfault.com/?enc=VESCEM75MWanV5Z3l8pzJg%3D%3D.I6hoZNQiYY3GPZ%2BUQqBXyeKNO3%2FSb1Si2NqpZmA25D%2FWB9bsjC0ucFqfaTeqiac6" rel="nofollow">starter kit</a>,在这里我们给出了团队在众多项目实践总结出的统一解决方案(NOWA),供你参考。<br><img src="https://gw.alicdn.com/tps/TB1ZkmxKFXXXXbHXXXXXXXXXXXX-638-385.png" alt="图4 nowa 工具" title="图4 nowa 工具"></p>
<h2>2. UXCore 正在做的事情</h2>
<h3>2.1 更加值得信赖</h3>
<p><img src="https://gw.alicdn.com/tps/TB1aVGeKFXXXXa0XVXXXXXXXXXX-820-254.png" alt="图5 持续集成" title="图5 持续集成"></p>
<blockquote><p>为了保证后续迭代和社区贡献的质量,我们会积极地接入代码风格校验和持续集成测试,目前我们的方案已经确定,正在向所有的组件进行推广。</p></blockquote>
<h3>2.2 可视化页面搭建</h3>
<p>通过在线可视化的页面,通过拖拽和简单配置来实现简单交互页面的搭建,同时输出代码,用以后续更改和添加代码。</p>
<h3>2.3 持续的功能增强</h3>
<p>借助社区和团队自身的力量,UXCore 会在现有基础上优化使用体验,增强组件功能,添加更多常用组件来满足不同场景的需求。</p>
<h2>3. 链接</h2>
<ul>
<li><p>github: <a href="https://link.segmentfault.com/?enc=Kt%2FXA3JxdQVcjwgHqbXgPQ%3D%3D.kpSAznPir3ZCp9Bsao0jHGqpfDOzZib3jKogkldCKtBAHrMyc3X9LrfAVNR9Pb0%2B" rel="nofollow">https://github.com/uxcore/uxcore</a></p></li>
<li><p>文档站点: <a href="https://link.segmentfault.com/?enc=IURW7zpwJqBflWJfCg%2FViA%3D%3D.yYubHEQcikm4YENIaeZLcA%3D%3D" rel="nofollow">http://uxco.re/</a></p></li>
</ul>
开发工具心得:如何 10 倍提高你的 Webpack 构建效率
https://segmentfault.com/a/1190000005770042
2016-06-21T21:45:05+08:00
2016-06-21T21:45:05+08:00
紅白
https://segmentfault.com/u/eternalsky
74
<h2>0. 前言</h2>
<p><img src="/img/remote/1460000005770045" alt="babel+webpack+es6+react" title="babel+webpack+es6+react"></p>
<blockquote><p>图1:ES6 + Webpack + React + Babel</p></blockquote>
<p>webpack 是个好东西,和 NPM 搭配起来使用管理模块实在非常方便。而 Babel 更是神一般的存在,让我们在这个浏览器尚未全面普及 ES6 语法的时代可以先一步体验到新的语法带来的便利和效率上的提升。在 React 项目架构中这两个东西基本成为了标配,但 commonjs 的模块必须在使用前经过 webpack 的构建(后文称为 build)才能在浏览器端使用,而每次修改也都需要重新构建(后文称为 rebuild)才能生效,如何提高 webpack 的构建效率成为了提高开发效率的关键之一。</p>
<h2>1. Webpack 的构建流程</h2>
<p>在开始正式的优化之前,让我们先回顾一下 Webpack 的构建流程,有哪些关键步骤,只有了解了这些,我们才能分析出哪些地方有优化的可能性。<br><img src="/img/remote/1460000004839887" alt="webpack official" title="webpack official"></p>
<blockquote><p>图2:webpack is a module bundler.</p></blockquote>
<p>首先,我们来看看官方对于 Webpack 的理念阐释,webapck 把所有的静态资源都看做是一个 module,通过 webpack,将这些 module 组成到一个 bundle 中去,从而实现在页面上引入一个 bundle.js,来实现所有静态资源的加载。所以详细一点看,webpack 应该是这样的:</p>
<p><img src="/img/remote/1460000005770047" alt="" title=""></p>
<blockquote><p>图3:Every static asset should be able to be a module --webpack</p></blockquote>
<p>通过 loader,webpack 可以把各种非原生 js 的静态资源转换成 JavaScript,所以理论上任何一种静态资源都可以成为一个 module。 <br>当然 webpack 还有很多其他好玩的特性,但不是本文的重点因此不铺开进行说明了。了解了上述的过程,我们就可以根据这些过程的前后处理进行对应的优化,接下来我们会针对 build 和 rebuild 的过程给与相应的意见。</p>
<h2>2. RESOLVE</h2>
<p>我们先从解析模块路径和分析依赖讲起,有人可能觉得这无所谓,但当项目应用依赖的模块越来越多,越来越重时,项目越来越大,文件和文件夹越来越多时,这个过程就变得越来越关乎性能。</p>
<h3>2.1 减小 Webpack 覆盖的范围</h3>
<blockquote><p>build +, rebuild +</p></blockquote>
<p>webpack 默认会去寻找所有 resolve.root 下的模块,但是有些目录我们是可以明确告知 webpack 不要管这里,从而减轻 webpack 的工作量。这时会用到 <code>module.noParse</code> 参数。</p>
<h3>2.2 Resolove.root VS Resolove.moduledirectories</h3>
<blockquote><p>build +, rebuild +</p></blockquote>
<p><code>root</code> 和 <code>moduledirectories</code> 如果只从用法上来看,似乎是可以互相替代的。但因为 <code>moduledirectories</code> 从设计上是取相对路径,所以比起 <code>root</code> ,所以会多 parse 很多路径。</p>
<pre><code class="js">resolve: {
root: path.resolve('src/node_modules'),
extensions: ['', '.js', '.jsx']
},
resolve: {
modulesDirectories: ['node_modules', './src'],
extensions: ['', '.js', '.jsx']
},</code></pre>
<p>上面的配置,只会解析</p>
<pre><code class="sh">./src/node_modules/a</code></pre>
<p>==== 此处有修改 2016/09/10 感谢 <a href="/u/lili_21">@lili_21</a> ====</p>
<p>而下面的配置会解析</p>
<pre><code class="sh">/some/folder/structure/node_modules/a
/some/folder/structure/src/a
/some/folder/node_modules/a
/some/folder/src/a
/some/node_modules/a
/some/src/a
/node_modules/a
/src/a </code></pre>
<p>大部分的情况下使用 <code>root</code> 即可,只有在有很复杂的路径下,才考虑使用 <code>moduledirectories</code>,这可以<a href="https://link.segmentfault.com/?enc=fyj95uDgHMPl5q9YKNqDnQ%3D%3D.kVv%2FokvtTArUDrVo0DJIflbklLp7IlUw2SJWAky4cK6sjT1%2BNFvSKGsZqxJtv5Kdha477gBFAp640nzhR91qPLaJz403L3Z%2FVD%2FROPFJLwI%3D" rel="nofollow">明显提高 webpack 的构建性能</a>。这个 <a href="https://link.segmentfault.com/?enc=dTbIzCRC8pcPG4JpswwD4A%3D%3D.uPLfs6Zol%2Bw%2FnrZXQTzg8plgbxxkUlZK2O3AAjyzEVe9MZ14cVLh%2F1V13IEEPkZHjAeQsMXnDzWVSJPqoJecBwGLHMyHvTydLpwPMWlCV48%3D" rel="nofollow">issue</a> 也很详细地讨论了这个问题。</p>
<h2>3. LOADERS</h2>
<p>webpack 官方和社区为我们提供了各种各样 loader 来处理各种类型的文件,这些 loader 的配置也直接影响了构建的性能。</p>
<h3>3.1 Babel-loader: 能者少劳</h3>
<blockquote><p>build ++, rebuild ++</p></blockquote>
<p>以 babel-loader 为例,我们在开发 React 项目时很可能会使用到了 ES6 或者 jsx 的语法,因此使用到 babel-loader 的情况很多,最简单的情况下我们可以这样配置,让所有的 js/jsx 通过 babel-loader:</p>
<pre><code>module: {
loaders: [
{
test: /\.js(x)*$/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015-ie', 'stage-1']
}
}
]
}</code></pre>
<p>上面这样的做法当然是 ok 的,但是对于很多的 npm 包来说,他们完全没有经过 babel 的必要(成熟的 npm 包会在发布前将自己 es5,甚至 es3 化),让这些包通过 babel 会带来巨大的性能负担,毕竟 babel6 要经过几十个插件的处理,虽然 babel-loader 强大,但能者多劳的这种保守的想法却使得 babel-loader 成为了整个构建的性能瓶颈。所以我们可以使用 <code>exclude</code>,大胆地屏蔽掉 npm 里的包,从而使整包的构建效率飞速提高。</p>
<pre><code>module: {
loaders: [
{
test: /\.js(x)*$/,
loader: 'babel-loader',
exclude: function(path) {
// 路径中含有 node_modules 的就不去解析。
var isNpmModule = !!path.match(/node_modules/);
return isNpmModule;
},
query: {
presets: ['react', 'es2015-ie', 'stage-1']
}
}
]
}</code></pre>
<p>甚至,在我们十分确信的情况下,使用 include 来限定 babel 的使用范围,进一步提高效率。</p>
<pre><code>var path = require('path');
module.exports = {
module: {
loaders: [
{
test: /\.js(x)*$/,
loader: 'babel-loader',
include: [
// 只去解析运行目录下的 src 和 demo 文件夹
path.join(process.cwd(), './src'),
path.join(process.cwd(), './demo')
],
query: {
presets: ['react', 'es2015-ie', 'stage-1']
}
}
]
}
}</code></pre>
<h2>4. PLUGINS</h2>
<p>webpack 官方和社区为我们提供了很多方便的插件,有些插件为我们开发和生产带来了很多的便利,但是不合适地使用插件也会拖慢 webpack 的构建效率,而有些插件虽然不会为我们的开发上直接提供便利,但使用他们却可以帮助我们提高 webpack 的构建效率,这也是本文会提到的。</p>
<h3>4.1 SourceMaps</h3>
<blockquote><p>build +</p></blockquote>
<p>SourceMaps 是一个非常实用的功能,可以让我们在 chrome debug 时可以不用直接看已经 bundle 过的 js,而是直接在源代码上进行查看和调试,但完美的 SourceMaps 是很慢的,webpack 官方提供了七种 sourceMap 模式共大家选择,性能对比如下:</p>
<table>
<thead><tr>
<th>devtool</th>
<th>build speed</th>
<th>rebuild speed</th>
<th>production supported</th>
<th>quality</th>
</tr></thead>
<tbody>
<tr>
<td>eval</td>
<td>+++</td>
<td>+++</td>
<td>no</td>
<td>generated code</td>
</tr>
<tr>
<td>cheap-eval-source-map</td>
<td>+</td>
<td>++</td>
<td>no</td>
<td>transformed code (lines only)</td>
</tr>
<tr>
<td>cheap-source-map</td>
<td>+</td>
<td>o</td>
<td>yes</td>
<td>transformed code (lines only)</td>
</tr>
<tr>
<td>cheap-module-eval-source-map</td>
<td>o</td>
<td>++</td>
<td>no</td>
<td>original source (lines only)</td>
</tr>
<tr>
<td>cheap-module-source-map</td>
<td>o</td>
<td>-</td>
<td>yes</td>
<td>original source (lines only)</td>
</tr>
<tr>
<td>eval-source-map</td>
<td>--</td>
<td>+</td>
<td>no</td>
<td>original source</td>
</tr>
<tr>
<td>source-map</td>
<td>--</td>
<td>--</td>
<td>yes</td>
<td>original source</td>
</tr>
</tbody>
</table>
<p>具体各自的区别请参考 <a href="https://link.segmentfault.com/?enc=c8QrbiCaH9hF1lSViQ4OCA%3D%3D.190LehnYNNC1KnOLHdUnI46axVyC7BVoG4J7XpsSeWxfTTf9DbGWRhuF7t8ns%2FVhcyNs07vMoJcr%2FLaNz7mr%2FA%3D%3D" rel="nofollow">https://github.com/webpack/do...</a> ,我们这里推荐使用 cheap-source-map,也就是去掉了column mapping 和 loader-sourceMap(例如 jsx to js) 的 sourceMap,虽然带上 <code>eval</code> 参数的可以快更多,但是这种 sourceMap 只能看,不能调试,得不偿失。</p>
<h3>4.2 OPTIMIZATION</h3>
<blockquote><p>build ++,rebuild ++</p></blockquote>
<p>webpack 提供了一些可以优化浏览器端性能的优化插件,如UglifyJsPlugin,OccurrenceOrderPlugin 和 DedupePlugin,都很实用,也都在消耗构建性能(UglifyJsPlugin 非常耗性能),如果你是在开发环境下,这些插件最好都不要使用,毕竟脚本大一些,跑的慢一些这些比起每次构建要耗费更多时间来说,显然还是后者更会消磨开发者的耐心,因此,只在正产环境中使用 OPTIMIZATION。</p>
<h3>4.3 CommonsChunk</h3>
<blockquote><p>rebuild +</p></blockquote>
<p>当你的 webpack 构建任务中有多个入口文件,而这些文件都 require 了相同的模块,如果你不做任何事情,webpack 会为每个入口文件引入一份相同的模块,显然这样做,会使得相同模块变化时,所有引入的 entry 都需要一次 rebuild,造成了性能的浪费,CommonsChunkPlugin 可以将相同的模块提取出来单独打包,进而减小 rebuild 时的性能消耗。这里有一篇很通俗易懂的使用方法:<a href="https://link.segmentfault.com/?enc=ampEiSkMTaslIumQEMrMbQ%3D%3D.IpszHesm6VHw8FhGeD4Whs%2BXlB%2FiNI7slV8aNsw250HJ2gjlG45MYB3QD%2Bn8f50b2ygCyPUzZ8gH4ALRsbGhXCxyht6ZneBD2tBoHCHpwBA%3D" rel="nofollow">http://webpack.toobug.net/zh-...</a> ,感兴趣的朋友不妨一试。</p>
<h3>4.4 DLL & DllReference</h3>
<blockquote><p>build +++, rebuild +++</p></blockquote>
<p>除了正在开发的源代码之外,通常还会引入很多第三方 NPM 包,这些包我们不会进行修改,但是仍然需要在每次 build 的过程中消耗构建性能,那有没有什么办法可以减少这些消耗呢?DLLPlugin 就是一个解决方案,他通过前置这些依赖包的构建,来提高真正的 build 和 rebuild 的构建效率。<br>鉴于现有的资料对于这两个插件的解释都不是很清楚,笔者这里翻译了一篇<a href="https://link.segmentfault.com/?enc=GECOB0sETpr%2BmbnO0zusBg%3D%3D.8TugCNCDiQn8bfwMiVOzQi6rJG54D%2BjvXRk%2FJ7M5llIGrhgGKQ%2FFLQcYyGX7clHwURJBJZ88v%2BdZVdPPKGWWAzOjQXc0cV9TGHfGsQfgYNL8n2FTTOwCUsNAXr9m0AAl8b3aQPGEcVgb6vkyGc6cGwKIzJXSnvgmRRk%2FIh9UuUY%3D" rel="nofollow">日本同学的文章</a>,通过一个简单的例子来说明一下这两个插件的用法。我们举例,把 react 和 react-dom 打包成为 dll bundle。<br>首先,我们来写一个 <a href="https://link.segmentfault.com/?enc=R7hHuZNufLGeZBX9EM%2Fedg%3D%3D.ywbPlEQTFZJhoXtK0uIoyI81fvS0glQ2IsoNtgoHobxVXGniMg9ZWMGbgDuDKfD5wiq%2Fxll%2Fb%2Fi1AorWgNMJuw%3D%3D" rel="nofollow">DLLPlugin</a> 的 config 文件。</p>
<blockquote><p>webpack.dll.config.js</p></blockquote>
<pre><code class="js">const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
vendor: ['react', 'react-dom']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
/**
* output.library
* 将会定义为 window.${output.library}
* 在这次的例子中,将会定义为`window.vendor_library`
*/
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
/**
* path
* 定义 manifest 文件生成的位置
* [name]的部分由entry的名字替换
*/
path: path.join(__dirname, 'dist', '[name]-manifest.json'),
/**
* name
* dll bundle 输出到那个全局变量上
* 和 output.library 一样即可。
*/
name: '[name]_library'
})
]
};</code></pre>
<p>执行 webpack 后,就会在 dist 目录下生成 dll bundle 和对应的 manifest 文件</p>
<pre><code class="sh">$ ./node_modules/.bin/webpack --config webpack.dll.config.js
Hash: 36187493b1d9a06b228d
Version: webpack 1.13.1
Time: 860ms
Asset Size Chunks Chunk Names
vendor.dll.js 699 kB 0 [emitted] vendor
[0] dll vendor 12 bytes {0} [built]
+ 167 hidden modules
$ ls dist
./ vendor-manifest.json
../ vendor.dll.js</code></pre>
<p>manifest 文件的格式大致如下,由包含的 module 和对应的 id 的键值对构成。</p>
<pre><code class="sh">cat dist/vendor-manifest.json
{
"name": "vendor_library",
"content": {
"./node_modules/react/react.js": 1,
"./node_modules/react/lib/React.js": 2,
"./node_modules/process/browser.js": 3,
"./node_modules/object-assign/index.js": 4,
"./node_modules/react/lib/ReactChildren.js": 5,
"./node_modules/react/lib/PooledClass.js": 6,
"./node_modules/fbjs/lib/invariant.js": 7,
...</code></pre>
<p>好,接下来我们通过 <a href="https://link.segmentfault.com/?enc=FtT9xQSeqdR%2FzNMbc5%2FM5g%3D%3D.rYbVCtk%2BMT%2F2JtEUQREfogMpWPbMS0%2FV%2BDF0mrIYxryFM3qSpGil%2BPThupgKbQgTcQQ6A8Q75odj3P2O6jRlfuD1yHobrsNMJCdOKBlAfHQ%3D" rel="nofollow">DLLReferencePlugin</a> 来使用刚才生成的 DLL Bundle。</p>
<p>首先我们写一个只去 <code>require</code> react,并通过 <code>console.log</code> 吐出的 <code>index.js</code>。</p>
<pre><code class="js">var React = require('react');
var ReactDOM = require('react-dom');
console.log("dll's React:", React);
console.log("dll's ReactDOM:", ReactDOM);</code></pre>
<p>再写一个不参考 Dll Bundle 的普通 webpack config 文件。</p>
<blockquote><p>webpack.conf.js</p></blockquote>
<pre><code class="js">const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
'dll-user': ['./index.js']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].bundle.js'
}
};</code></pre>
<p>执行 webpack,会在 dist 下生成 dll-user.bundle.js,约 700K,耗时 801ms。</p>
<pre><code class="sh">$ ./node_modules/.bin/webpack
Hash: d8cab39e58c13b9713a6
Version: webpack 1.13.1
Time: 801ms
Asset Size Chunks Chunk Names
dll-user.bundle.js 700 kB 0 [emitted] dll-user
[0] multi dll-user 28 bytes {0} [built]
[1] ./index.js 145 bytes {0} [built]
+ 167 hidden modules</code></pre>
<p>接下来,我们加入 <a href="https://link.segmentfault.com/?enc=QictWAN9a4iRwDdoRQwtKw%3D%3D.X4U5T1xA%2FQ0SCbTkN2Gtep%2BFgWCz2ivVWSj1a1npz1tcyuBTbTlk%2BdoG6N0j4FJyPYkd8CHht%2B7NauTgFAT%2Fh0gP3dY8iicGAhZ3SABn0%2Bk%3D" rel="nofollow">DLLReferencePlugin</a></p>
<blockquote><p>webpack.conf.js</p></blockquote>
<pre><code class="js">const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
'dll-user': ['./index.js']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].bundle.js'
},
// ----在这里追加----
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
/**
* 在这里引入 manifest 文件
*/
manifest: require('./dist/vendor-manifest.json')
})
]
// ----在这里追加----
};</code></pre>
<pre><code class="sh">./node_modules/.bin/webpack
Hash: 3bc7bf760779b4ca8523
Version: webpack 1.13.1
Time: 70ms
Asset Size Chunks Chunk Names
dll-user.bundle.js 2.01 kB 0 [emitted] dll-user
[0] multi dll-user 28 bytes {0} [built]
[1] ./index.js 145 bytes {0} [built]
+ 3 hidden modules</code></pre>
<p>结果是非常惊人的,只有2.01K,耗时 70 ms,无疑大大提高了 build 和 rebuild 的效率。实际放到页面上看下是否可行。</p>
<pre><code class="html"><body>
<script src="dist/vendor.dll.js"></script>
<script src="dist/dll-user.bundle.js"></script>
</body></code></pre>
<p><img src="/img/remote/1460000005770049" alt="" title=""></p>
<p>因为 Dll bundle 在依赖安装完毕后就可以进行了,我们可以在第一次执行 dev server 前执行一次 dll bundle 的 webapck 任务。</p>
<h4>4.4.1 和 external 的比较</h4>
<p>有人会说,这个和 用 <code>webpack</code> 的 <code>externals</code> 配置把 require 的 module 指向全局变量有点像啊。</p>
<pre><code class="js">const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
'ex': ['./index.js']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].bundle.js'
},
externals: {
// require('react')はwindow.Reactを使う
'react': 'React',
// require('react-dom')はwindow.ReactDOMを使う
'react-dom': 'ReactDOM'
}
};</code></pre>
<pre><code class="html"><body>
<script src="dist/react.min.js"></script>
<script src="dist/react-dom.min.js"></script>
<script src="dist/ex.bundle.js"></script>
</body></code></pre>
<p>这里有两个主要的区别:</p>
<ol>
<li><p>像是 <code>react</code> 这种已经打好了生产包的使用 <code>externals</code> 很方便,但是也有很多 npm 包是没有提供的,这种情况下 <code>DLLBundle</code> 仍可以使用。</p></li>
<li><p>如果只是引入 npm 包一部分的功能,比如 <code>require('react/lib/React')</code> 或者 <code>require('lodash/fp/extend')</code> ,这种情况下 <code>DLLBundle</code> 仍可以使用。</p></li>
<li><p>当然如果只是引用了 <code>react</code> 这类的话,<code>externals</code> 因为配置简单所以也推荐使用。</p></li>
</ol>
<h3>4.5 <a href="https://link.segmentfault.com/?enc=bLu9yYpoWCixcIWHRUC0VA%3D%3D.6RH3TRlQ2XiS7heaKxhuPnZn3ufL4M4iH0oegmDWgT%2Fhc4XjTi6SKofdBT364UqY" rel="nofollow">HappyPack</a>
</h3>
<blockquote><p>build +, rebuild +</p></blockquote>
<p>webpack 的长时间构建搞的大家都很 unhappy。于是 @amireh 想到了一个点子,既然 loader 默认都是一个进程在跑,那是否可以让 loader 多进程去处理文件呢?</p>
<p><img src="/img/remote/1460000005770054" alt="" title=""></p>
<p>happyPack 的文档写的很易懂,这里就不再赘述,happyPack 不仅利用了多进程,同时还利用缓存来使得 rebuild 更快。下面是插件作者给出的性能数据:</p>
<blockquote><p>For the main repository I tested on, which had around 3067 modules, the build time went down from 39 seconds to a whopping ~10 seconds when there was yet no</p></blockquote>
<ol><li><p>Successive builds now take between 6 and 7 seconds.</p></li></ol>
<blockquote><p>Here's a rundown of the various states the build was performed in:</p></blockquote>
<table>
<thead><tr>
<th>Elapsed (ms)</th>
<th>Happy?</th>
<th>Cache enabled?</th>
<th>Cache present?</th>
<th colspan="2">Using DLLs?</th>
</tr></thead>
<tbody>
<tr>
<td>39851</td>
<td>NO</td>
<td>N/A</td>
<td>N/A</td>
<td colspan="2">NO</td>
</tr>
<tr>
<td>37393</td>
<td>NO</td>
<td>N/A</td>
<td>N/A</td>
<td colspan="2">YES</td>
</tr>
<tr>
<td>14605</td>
<td>YES</td>
<td>NO</td>
<td>N/A</td>
<td colspan="2">NO</td>
</tr>
<tr>
<td>13925</td>
<td>YES</td>
<td>YES</td>
<td>NO</td>
<td colspan="2">NO</td>
</tr>
<tr>
<td>11877</td>
<td>YES</td>
<td>YES</td>
<td>YES</td>
<td colspan="2">NO</td>
</tr>
<tr>
<td>9228</td>
<td>YES</td>
<td>NO</td>
<td>N/A</td>
<td colspan="2">YES</td>
</tr>
<tr>
<td>9597</td>
<td>YES</td>
<td>YES</td>
<td>NO</td>
<td colspan="2">YES</td>
</tr>
<tr>
<td>6975</td>
<td>YES</td>
<td>YES</td>
<td>YES</td>
<td colspan="2">YES</td>
</tr>
</tbody>
</table>
<blockquote><p>The builds above were run on Linux over a machine with 12 cores.</p></blockquote>
<h2>5. 其他</h2>
<p>上面我们针对 webpack 的 resolve、loader 和 plugin 的过程给出了相应的优化意见,除了这些哪些优化点呢?其实有些优化贯穿在这个流程中,比如缓存和文件 IO。</p>
<h3>5.1 Cache</h3>
<p>无论在何种性能优化中,缓存总是必不可少的一部分,毕竟每次变动都只影响很小的一部分,如果能够缓存住那些没有变动的部分,直接拿来使用,自然会事半功倍,在 webpack 的整个构建过程中,有多个地方提供了缓存的机会,如果我们打开了这些缓存,会大大加速我们的构建,尤其是 rebuild 的效率。</p>
<h4>5.1.1 <a href="https://link.segmentfault.com/?enc=4aDYN%2B%2BoYJiDzs2BNbG6oQ%3D%3D.tWgzzDzx%2FkYZ56%2FJWm6WFFbkhDneqHNkTVj77lvT%2FWZMQOLJUYd2ouz8RyictvgSQiKt9SSw1pUckH7Tgp%2BFgQ%3D%3D" rel="nofollow">webpack.cache</a>
</h4>
<blockquote><p>rebuild +</p></blockquote>
<p>webpack 自身就有 cache 的配置,并且在 watch 模式下自动开启,虽然效果不是最明显的,但却对所有的 module 都有效。</p>
<h4>5.1.2 <a href="https://link.segmentfault.com/?enc=oF%2Fi4d2kQnvnQEVfp0P0lw%3D%3D.VnulXxducsPdEGT6Pjz94%2FWnMvv%2BBh5oGtHZV8vm7ywF2l8866c4eFyny0mdHdg4" rel="nofollow">babel-loader.cacheDirectory</a>
</h4>
<blockquote><p>rebuild ++</p></blockquote>
<p>babel-loader 可以利用系统的临时文件夹缓存经过 babel 处理好的模块,对于 rebuild js 有着非常大的性能提升。</p>
<h4>5.1.3 <a href="https://link.segmentfault.com/?enc=qrGMQTyKGV6Z3%2By0TsMPqQ%3D%3D.P3woCdnIpE%2Bj4fkEZRJJC4QVjzrYxVH00nj9DYOH%2FsF9RLgOasf%2BTQOw7AOnYkbZ1b9b%2FdtHB6jNtW5RsjjsDw%3D%3D" rel="nofollow">HappyPack.cache</a>
</h4>
<blockquote><p>build +, rebuild +</p></blockquote>
<p>上面提到的 happyPack 插件也同样提供了 cache 功能,默认是以 <code>.happypack/cache--[id].json</code> 的路径进行缓存。因为是缓存在当前目录下,所以他也可以辅助下次 build 时的效率。</p>
<h3>5.2 FileSystem</h3>
<p>默认的情况下,构建好的目录一定要输出到某个目录下面才能使用,但 webpack 提供了一种很棒的读写机制,使得我们可以直接在内存中进行读写,从而极大地提高 IO 的效率,开启的方法也很简单。</p>
<pre><code class="js">var MemoryFS = require("memory-fs");
var webpack = require("webpack");
var fs = new MemoryFS();
var compiler = webpack({ ... });
compiler.outputFileSystem = fs;
compiler.run(function(err, stats) {
// ...
var fileContent = fs.readFileSync("...");
});</code></pre>
<p>当然,我们还可以通过 webpackDevMiddleware 更加无缝地就接入到 dev server 中,例如我们以 express 作为静态 server 的例子。</p>
<pre><code>var compiler = webpack(webpackCfg);
var webpackDevMiddlewareInstance = webpackDevMiddleware(compiler, {
// webpackDevMiddleware 默认使用了 memory-fs
publicPath: '/dist',
aggregateTimeout: 300, // wait so long for more changes
poll: true, // use polling instead of native watchers
stats: {
chunks: false
}
});
var app = express();
app.use(webpackDevMiddlewareInstance);
app.listen(xxxx, function(err) {
console.log(colors.info("dev server start: listening at " + xxxx));
if (err) {
console.error(err);
}
}</code></pre>
<h2>6. 总结</h2>
<p>上面我们从 webpack 构建的各个部分,给出了相应的优化策略,如果你的项目中能够将其完全贯彻起来,10 倍提速不是梦想。这些优化也同样应用到了我们团队的 react 项目中,<a href="https://link.segmentfault.com/?enc=wkJ3kov6hqME27iyNrhZiQ%3D%3D.4LxK8h0TUHZGFNv%2F2FpUqcKWzP%2BLNv7ocpjNR7w6yIhb7uQ%2BFBIqyZD970ofkhRI" rel="nofollow">https://github.com/uxcore/uxcore</a> ,欢迎一起来讨论 webpack 的效率优化方案。</p>
<h2>7. 参考文章</h2>
<ul>
<li><p>webpack build performance:<a href="https://link.segmentfault.com/?enc=i1ag%2Fu62kta%2FRxd%2BiLk%2FGQ%3D%3D.OxJfKCuiLmPQl5wlCZVLKZ%2B47W9LvjTd%2FvPB6Ynpz%2Fp3uXh4nWDOEKLquy%2B2F4RT71aEx2J1KloGCO2suiaZoQ%3D%3D" rel="nofollow">http://webpack.github.io/docs...</a></p></li>
<li><p>webpackのDLLバンドルを使ってビルドを速くする:<a href="https://link.segmentfault.com/?enc=bn%2FiY9cHHWNmzJ8SPuJqDQ%3D%3D.yJrP2LO6Lfjzcqap98G3NcHhGCHmfnHAJUNaDt85RV%2F3t%2BxWUrLaUvrPJqYxUdEo8P8R5h5pzUfeQituaJ5lDp%2BgKn1Ee4TKj%2FyFFcODnBXqX33FRPc27VDHxaxEFNAIogEtWPG0kcVaRFgytEJY%2Bw%3D%3D" rel="nofollow">http://qiita.com/pirosikick/i...</a></p></li>
<li><p>How to make your Webpack builds 10x faster:<a href="https://link.segmentfault.com/?enc=Dtm9AgsL2ABJ7ih9MHGNwA%3D%3D.7WVhUpN3r9cmZVD%2F6Pdg5vp8TkRzbkoHu278eukQ74c%2Bb9JVK6iBwzjq9aOoZsGNTHS1r8ziGnwYvBYjmJS%2FJd8VxuRdqhPMGCPR42A1jzU%3D" rel="nofollow">http://www.slideshare.net/tru...</a></p></li>
</ul>
<blockquote><p>本文作者 <a href="https://link.segmentfault.com/?enc=w4XAy06pf%2BL%2F1TIUaJGABw%3D%3D.NwnEFM3ffVy2vkHnNq%2F3t4VbdsGQ0EF82HtE9kfwy34%3D" rel="nofollow">eternalsky</a>,始发于团队微信公众号 <strong>猿猿相抱</strong> 和个人博客 <a href="https://link.segmentfault.com/?enc=Gtp5Kmft4BU8ciMIC0xwnQ%3D%3D.Lx1%2Fv4HyuEj%2FrXYKtFTdzfa1mUQ5hvHx%2BuvrfFGbaPM%3D" rel="nofollow"><strong>空の屋敷</strong></a>,转载请保留作者信息。</p></blockquote>