ansenhuang

ansenhuang 查看完整档案

杭州编辑西安电子科技大学  |  机械设计制造及其自动化 编辑字节跳动  |  前端 编辑 ansenhuang.github.io/ 编辑
编辑

frontend boy

个人动态

ansenhuang 发布了文章 · 2019-09-04

【译】不要再问我React Hooks能否取代Redux了

原文地址:Stop Asking if React Hooks Replace Redux

许多同事一直问我一些类似的问题:

“如果我们在项目中使用hooks,我们是否还需要Redux?”

“React Hooks会不会使Redux太过时了?我能不能用Hooks来做所有Redux能做的事呢?”

在Google中搜索会发现,大家经常问这些问题。

“React Hooks是否会取代Redux?”,最简单的回答是“不一定”。

更细致但礼貌的答案是“嗯,那取决于你正在做的项目类型”。

我更倾向于告诉大家的答案是“我不确定你是否知道你在说什么”。有几个原因可以说明,为什么“React Hooks是否会取代Redux”是一个本质上有缺陷的问题。首先:

Redux一直是非强制性的

通过Dan Abramov(Redux的创造者之一)的一篇文章【You Might Not Need Redux】可以看出,如果你不需要使用它,则无需替换任何东西。

Redux是一个JavaScript库,并且如果你用的是React(另一个JavaScript库),那么为了使用Redux,你还需要在应用中加载一个React-Redux的JavaScript库。在项目中使用依赖库会增加打包体积,这会增加你应用的加载时间。基于这个原因,你不应该使用一些库,像jQuery、Redux、MobX(另一个状态管理库),甚至是React,除非你有明确的理由要使用它们。

当大家问到“是否hooks会替代Redux”,他们似乎经常觉得,他们的React应用需要使用其中一种。事实并非如此,如果你正在写的应用没有很多状态需要被储存,或者你的组件结构很简单,可以避免过度的prop传递,以及基于React本身提供的特性,你的状态已经足够可控了,不管有没有hooks,这些情况使用状态管理就没有多大意义了。

即使你确实有许多的状态,或者有像老树根一样扭曲分叉的React组件结构,你仍然不需要状态管理库。Prop传递可能很麻烦,但是React给了你许多状态管理选项,并且hooks绝对可以帮你很好地组织状态。Redux是一个轻量级的库,但是它的设置很复杂,增加了打包体积,并且很多地方需要权衡。有很多原因可以说明,为什么你应该选择不在项目中使用它,并且这些原因很有说服力。

你并不总是需要Redux,这也是在说,你依然有许多理由去使用它的。如果你的项目在一开始就使用了Redux,那么它可能是一个很好的理由,无论它是否做了这些:组织(应用状态的可预测性、单一的数据流,在复杂的应用中很有用)、中间件、Redux的强有力的开发工具和调试能力。如果你有使用Redux的理由,它不会因为React Hooks变得无效。如果你之前需要Redux,那么你现在仍然需要。这是因为:

React Hooks和Redux并没有试图解决同样的问题

Redux是一个状态管理库,Hooks是React最近更新的部分特性,让你的函数组件可以做类组件能做的事情。

所以不使用类组件来写React应用突然会让状态管理库变得过时了呢?

当然不会!

通过文档可以看出,React Hooks被开发出来主要是这三个理由:

  • 难以复用类组件之间的逻辑
  • 生命周期中经常包含一些莫名其妙的不相关逻辑
  • 类组件难以被机器和人理解

注意,没有一条理由的动机直接表明要做一些与状态管理相关的事情。

话说如此,React Hooks确实提供了一些选择去管理应用的状态。尤其是useStateuseReduceruseContext方法,提供来新的方式去维护你的状态,这被证明比先前React提供的选项更好、更有条理。

但是这些hooks并不是什么新东西或神奇的东西,并且它们也没有使状态管理过时,因为事实是:

React Hooks并没有让你的应用可以做一些以前做不到的事情

那就对了,你现在可以写函数组件来做一些以前只能用类组件来做的事情,但是这些函数组件并不能做一些类组件做不到的事情,除了可以更好地组织和复用代码的能力。它们不一定让你的应用更好,而是让开发者的体验更好。

useStateuseReducer只是管理组件状态的方法,并且它们的工作原理同类组件的this.statethis.setState是一样的,你仍然需要传递你的props。

useContext是大家认为在Redux板上钉钉的特性,因为它可以让你在组件之间共享应用的状态,而不需要通过prop传递,但是它也没有真正的做任何新的事情。context API现在是React的一部分,useContext仅仅是让你不用<Consumer>包裹也可以使用context。并且有一些开发这用context来管理整个应用的状态,这不是设计context的目的。通过文档可以看出:

Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.

Context是为了共享数据而被设计出来的,可以认为是React组件树的“全局”,比如当前已授权的用户、主题或者首选的语言。

换句话说,就是那些预计不会频繁更新的东西。

文档中也建议有节制地使用context,因为“它会使得组件难以复用”。他们也提醒开发者,如果开发者不小心,context很容易触发不必要的重复渲染。

我见过项目成功地使用React Context来管理应用状态,这是有可能的,也不失为一种选择。但是状态管理并不完全是context被设计出来去做的事情,而且Redux和其他状态管理库被设计出来就是为了处理这种特定的目的。

此外,React Hooks也绝不意味着Redux的消亡,因为如果你看一眼React-Redux最近更新的文档,你会明白:

React-Redux也有自己的hooks

没错,React Hooks正在帮助React-Redux恢复活力并移除来它的一些痛点,与“替代”的说法相差甚远。

我在另一篇文章中对React-Redux进行了深入研究,这里要说的重点。在hooks之前,你必须定义mapStateToPropsmapDispatchToProps两个函数,并且用connect包裹你的组件来创建一个高阶组件,它会传递dispatch方法和部分Redux贮存的状态,这些状态是你在mapping函数中指定作为props传递到组件中的。

让我们来看一个非常简单的计数器应用的例子(太简单甚至都不需要Redux,但是这里主要是为了展示一些信息)。假设我们已经定义了Redux store和incrementdecrement两个action creator(完整的源码在这里)。

import React from 'react';
import {connect} from 'react-redux';
import * as actions from '../actions/actions';

class App extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    const {count, increment, decrement} = this.props;

    return (
      <div>
        <h1>The count is {count}</h1>
        <button onClick={() => increment(count)}>+</button>
        <button onClick={() => decrement(count)}>-</button>
      </div>
    );
  }
}

const mapStateToProps = store => ({
  count: store.count
});

const mapDispatchToProps = dispatch => ({
  increment: count => dispatch(actions.increment(count)),
  decrement: count => dispatch(actions.decrement(count))
});

export default connect(mapStateToProps, mapDispatchToProps)(App);

太令人烦恼了!如果我们不必包裹组件到高阶组件中,就可以让组件取到Redux store的值,这样不是更友好吗?是的,这就是hooks出现的原因。Hooks就是为了复用代码和消除由于高阶组件产生的“嵌套地狱”。下面是一个相同的组件,使用React-Redux hooks转换成函数组件。

import React from 'react';
import * as actions from '../actions/actions';
import {useSelector, useDispatch} from 'react-redux';

const App = () => {
  const dispatch = useDispatch();
  const count = useSelector(store => store.count);

  return (
    <div>
      <h1>The count is {count}</h1>
      <button onClick={() => dispatch(actions.increment(count))}>+</button>
      <button onClick={() => dispatch(actions.decrement(count))}>-</button>
    </div>
  );
}

export default App;

是不是很漂亮?简而言之,useSelector让你可以保存部分Redux store的值到你的组件。useDispatch更简单,它仅仅为你提供了一个dispatch函数,你可以用它来发送状态更新到Redux store。最棒的是,你不再需要写这些丑陋的mapping函数和用connect函数来包裹组件。现在,一切都很好地包含在你的组件中,它更简洁,因此更容易阅读,并且更有条理。重点是:

没有必要比较React Hooks和Redux孰优孰劣

毫无疑问,这两项技术可以很好地互补。React Hooks不会替代Redux,它们仅仅为你提供来新的、更好的方式去组织你的React应用。如果你最终决定使用Redux来管理状态,可以让你编写更好的连接组件。

所以,请不要再问“React Hooks是否会取代Redux?”。

相反,开始问自己“我正在制作什么样的应用?我需要什么样的状态管理?Redux可以用吗,还是有些过度使用呢?hooks可以用吗,还是应该用类组件?如果我决定使用Redux和React Hooks(或者MobX和React Hooks,或者Redux和jQuery,不用React——这些都是有效的选择,取决于你正在做的事情),那么我怎样可以使这些技术互补并且和谐共处呢?”。

查看原文

赞 2 收藏 1 评论 0

ansenhuang 发布了文章 · 2019-08-13

如何利用JavaScript的Map提升性能

在ES6中引入JavaScript的新特性中,我们看到了SetMap的介绍。与常规对象和Array不同的是,它们是“键控集合(keyed collections)”。这就是说它们的行为有稍许不同,并且在特定的上下文中使用,它们可以提供相当大的性能优势。

在这篇文章中,我将剖析Map,它究竟有何不同,哪里可以派上用场,相比于常规对象有什么性能优势。

Map与常规对象有什么不同

Map和常规对象主要有2个不同之处。

1.无限制的键(Key)

常规JavaScript对象的键必须是StringSymbol,下面的对象说明的这一点:

const symbol = Symbol();
const string2 = 'string2';

const regularObject = {
  string1: 'value1',
  [string2]: 'value2',
  [symbol]: 'value3'
};

相比之下,Map允许你使用函数、对象和其它简单的类型(包括NaN)作为键,如下代码:

const func = () => null;
const object = {};
const array = [];
const bool = false;
const map = new Map();

map.set(func, 'value1');
map.set(object, 'value2');
map.set(array, 'value3');
map.set(bool, 'value4');
map.set(NaN, 'value5');

在链接不同数据类型时,这个特性提供了极大的灵活性。

2.直接遍历

在常规对象中,为了遍历keys、values和entries,你必须将它们转换为数组,如使用Object.keys()Object.values()Object.entries(),或者使用for ... in循环,因为常规对象不能直接遍历,另外for ... in循环还有一些限制:它仅仅遍历可枚举属性、非Symbol属性,并且遍历的顺序是任意的。

Map可以直接遍历,并且由于它是键控集合,遍历的顺序和插入键值的顺序是一致的。你可以使用for ... of循环或forEach方法来遍历Map的entries,如下代码:

for (let [key, value] of map) {
  console.log(key);
  console.log(value);
};
map.forEach((key, value) => {
  console.log(key);
  console.log(value);
});

还有一个好处就是,你可以调用map.size属性来获取键值数量,而对于常规对象,为了做到这样你必须先转换为数组,然后获取数组长度,如:Object.keys({}).length

MapSet有何不同

Map的行为和Set非常相似,并且它们都包含一些相同的方法,包括:has、get、set、delete。它们两者都是键控集合,就是说你可以使用像forEach的方法来遍历元素,顺序是按照插入键值排列的。

最大的不同是Map通过键值(key/value)成对出现,就像你可以把一个数组转换为Set,你也可以把二维数组转换为Map

const set = new Set([1, 2, 3, 4]);
const map = new Map([['one', 1], ['two', 2], ['three', 3], ['four', 4]]);

类型转换

要将Map切换回数组,你可以使用ES6的结构语法:

const map = new Map([['one', 1], ['two', 2]]);
const arr = [...map];

到目前为止,将Map与常规对象的互相转换依然不是很方便,所以你可能需要依赖一个函数方法,如下:

const mapToObj = map => {
  const obj = {};
  map.forEach((key, value) => { obj[key] = value });
  return obj;
};
const objToMap = obj => {
  const map = new Map();
  Object.keys(obj).forEach(key => { map.set(key, obj[key]) });
  return map;
};

但是现在,在八月份ES2019的首次展示中,我们看见了Object引入了2个新方法:Object.entries()Object.fromEntries(),这可以使上述方法简化许多:

const obj2 = Object.fromEntries(map);
const map2 = new Map(Object.entries(obj));

在你使用Object.fromEntries转换map为object之前,确保map的key在转换为字符串时会产生唯一的结果,否则你将面临数据丢失的风险。

性能测试

为了准备测试,我会创建一个对象和一个map,它们都有1000000个相同的键值。

let obj = {}, map = new Map(), n = 1000000;
for (let i = 0; i < n; i++) {
  obj[i] = i;
  map.set(i, i);
}

然后我使用console.time()来衡量测试,由于我特定的系统和Node.js版本的原因,时间精度可能会有波动。测试结果展示了使用Map的性能收益,尤其是添加和删除键值的时。

查询

let result;
console.time('Object');
result = obj.hasOwnProperty('999999');
console.timeEnd('Object');
// Object: 0.250ms

console.time('Map');
result = map.has(999999);
console.timeEnd('Map');
// Map: 0.095ms (2.6 times faster)

添加

console.time('Object');
obj[n] = n;
console.timeEnd('Object');
// Object: 0.229ms

console.time('Map');
map.set(n, n);
console.timeEnd('Map');
// Map: 0.005ms (45.8 times faster!)

删除

console.time('Object');
delete obj[n];
console.timeEnd('Object');
// Object: 0.376ms

console.time('Map');
map.delete(n);
console.timeEnd('Map');
// Map: 0.012ms (31 times faster!)

Map在什么情况下更慢

在测试中,我发现一种情况常规对象的性能更好:使用for循环去创建常规对象和map。这个结果着实令人震惊,但是没有for循环,map添加属性的性能胜过常规对象。

console.time('Object');
for (let i = 0; i < n; i++) {
  obj[i] = i;
}
console.timeEnd('Object');
// Object: 32.143ms

let obj = {}, map = new Map(), n = 1000000;
console.time('Map');
for (let i = 0; i < n; i++) {
  map.set(i, i);
}
console.timeEnd('Map');
// Map: 163.828ms (5 times slower)

举个例子

最后,让我们看一个Map比常规对象更合适的例子,比如说我们想写一个函数去检查2个字符串是否由相同的字符串随机排序。

console.log(isAnagram('anagram', 'gramana')); // Should return true
console.log(isAnagram('anagram', 'margnna')); // Should return false

有许多方法可以做到,但是这里,map可以帮忙我们创建一个最简单、最快速的解决方案:

const isAnagram = (str1, str2) => {
  if (str1.length !== str2.length) {
    return false;
  }
  const map = new Map();
  for (let char of str1) {
    const count = map.has(char) ? map.get(char) + 1 : 1;
    map.set(char, count);
  }
  for (let char of str2) {
    if (!map.has(char)) {
      return false;
    }
    const count = map.get(char) - 1;
    if (count === 0) {
      map.delete(char);
      continue;
    }
    map.set(char, count);
  }
  return map.size === 0;
};

在这个例子中,当涉及到动态添加和删除键值,无法提前确认数据结构(或者说键值的数量)时,map比object更合适。


我希望这篇文章对你有所帮助,如果你之前没有使用过Map,不妨开阔你的眼界,衡量现代JavaScript的价值体现。

译者注:我个人不太同意作者的观点,从以上的描述来看,Map更像是以空间为代价,换取速度上的提升。那么对于空间和速度的衡量,必然存在一个阈值。在数据量比较少时,相比与速度的提升,其牺牲的空间代价更大,此时显然是不适合使用Map;当数据量足够大时,此时空间的代价影响更小。所以,看开发者如何衡量两者之间的关系,选择最优解。

原文链接

查看原文

赞 0 收藏 0 评论 0

ansenhuang 发布了文章 · 2019-08-09

前端UI自动化测试(TypeScript+Jest+Puppeteer)

最近几个月来,笔者一直在探索前端UI自动化测试的场景和方案。最初的时候,面对众多的技术选型,显得有些茫然,而团队此前也没有太多关于这方面的经验,只能一步一步摸索总结,当然期间也踩过不少坑,最终形成了一套相对稳定的测试方案,未来还将继续扩展和完善。

项目地址:jest-puppeteer-testing

在这篇文章中,笔者想和大家分享一下自己对于UI自动化测试的思考和经验。

为什么要进行UI自动化测试

业务的更新迭代频繁,传统测试大部分都还是手工、肉眼的模式来进行,无法满足产品敏捷开发、快速迭代的需求。而UI自动化能让全功能的回归变得简单,释放纯手工测试的人力资源,并且回归测试能够覆盖到所有的逻辑场景,这对测试的效率,以及整个开发流程的效率都是很大的提升,并且能够规避很多人的主观和客观因素导致的漏测或者疏忽。

其他测试方式的局限性:

单元测试(Unit Testing)

事实上,单元测试确实能够帮助我们发现大部分的问题,但是在复杂的前端交互中,单纯的单元测试并不能真实地反映用户操作的路径,而单元测试一般的场景是测试一系列的功能集合。

快照测试(Snapshot Testing)

DOM结构并不能完全反映页面的视觉效果,DOM结构不变并不完全等于样式不变。此外,大多数工具都是React专用,非React应用基本不支持。

笔者想说:

很多人认为,UI总是频繁的变动,导致测试用例维护成本高,性价比低,因此UI自动化测试比较适合场景稳定的业务。其实不是,这里的UI不仅仅指的是视觉,更多的是业务逻辑。UI可以多变,但业务逻辑一定是趋于稳定的,尤其是核心业务,想一想用户得多辛苦才能适应这种业务逻辑频繁变更的产品啊。

关于技术选型

TypeScript + Jest + Puppeteer

事实上,对于UI自动化测试来说,许多框架之间并没有太多差别,也从来不是影响整套测试用例是否健壮的关键性因素。相比之下,如何提高测试用例稳定性及全面性才是让UI自动化测试方案落地的重要细节。

开发实践

项目搭建

大家可以参考jest-puppeteer-testing,这里不再累述。

核心文件

// setup/expect-image-snapshot.ts
// 让jest支持保存/比对屏幕截图
import { configureToMatchImageSnapshot } from 'jest-image-snapshot';

expect.extend({
  toMatchImageSnapshot: configureToMatchImageSnapshot({
    customSnapshotsDir: '__image_snapshots__',
  }),
});
// setup/enhance-puppeteer.ts
// 增强puppeteer功能,如:拦截请求并使用mock数据
import { onRequestInterceptor } from '../utils/request';

jest.setTimeout(30000);

beforeAll(async () => {
  page.on('request', onRequestInterceptor); // 拦截请求,使用代理数据
  await page.setRequestInterception(true);
});
// utils/request.ts
// mock数据的核心文件
// 这里只拦截xhr或fetch请求,当然你也可以自行扩展
import { URL } from 'url';
import { Request } from 'puppeteer';
import mocks from '../mocks';

// 设置请求拦截器的数据,用于同一请求返回不同结果,生效一次后自动销毁
export const interceptors: { [api: string]: any } = {};
export const setRequestInterceptor = (api: string, value: any) => {
  interceptors[api] = value;
};

export const onRequestInterceptor = (request: Request) => {
  const resourceType = request.resourceType();
  if (resourceType === 'xhr' || resourceType === 'fetch') {
    const location = new URL(request.url());
    const mockKey = location.pathname;
    if (mockKey && mocks.hasOwnProperty(mockKey)) {
      const mock = mocks[mockKey];
      let response: any;
      if (typeof mock === 'function') {
        response = mock({ location, request, interceptor: interceptors[mockKey] });
        delete interceptors[mockKey]; // 生效一次后自动销毁
      } else {
        response = mock;
      }
      if (response) {
        if (response.body != null && typeof response.body === 'object') {
          response.body = JSON.stringify(response.body);
        }
        request.respond(response);
      }
    } else {
      request.continue();
    }
  } else {
    request.continue();
  }
};

其他文件

  • shared.d.ts定义数据类型
  • cases目录下存放测试用例
  • mocks目录下存放mock数据
  • utils目录下存放工具方法

补充说明:关于mock的类型定义,可以在shared.d.ts中找到,当然你也可以在这里增加其他类型定义

经验总结

测试地址的选择(本地/线上)

  • 本地服务器:请求响应快,测试结果稳定,但无法排除由线上环境差异或代码打包过程中引发的问题
  • 线上服务器:能够反映网站真实的展示,无需额外启动服务器,任何时候都可以测试,但受网络因素影响,可能导致测试结果不稳定

尽量抹平不确定因素带来的影响

如维持数据请求的结果稳定,日期时间稳定,保证页面渲染的一致性。假如由于数据返回或时间的不确定性,导致每次页面渲染不一样,那这样测试也失去了意义。

尽量明确保存屏幕截图的时机

如访问一个页面后截图,由于网络因素的原因,图片资源并不是每次都加载完成,从而导致截图前后不一样。

......(暂时写这么多,有空再更)

总结

事实上,这套UI自动化测试方案更像是端到端测试(E2E Testing),即模拟一个用户将程序作为一个完全的黑盒,打开应用程序模拟输入,检查功能以及界面是否正确,配合屏幕截图可以直观感受到用户进行某些交互产生的具像化视觉效果。

项目地址:jest-puppeteer-testing
查看原文

赞 1 收藏 0 评论 0

ansenhuang 关注了专栏 · 2019-05-28

腾讯新闻前端团队

TNFE 腾讯新闻前端团队,专业的前端技术专栏,一线的前端技术文章。

关注 4708

ansenhuang 发布了文章 · 2019-04-08

A fascinating technique that can greatly reduce your code

It's a common way in most web developers that write JavaScript with ES6+ and then bundle it to ES5, so that it can run in all browsers. However, the modern browsers support ES6 natively so it's unnecessary to shipping a lot of polyfills.

I'm really excited to share you a technique that you can compile and serve two separate JavaScript bundles:

  • One bundle you are definitely already generating, which serve for legacy browsers with polyfills.
  • Another bundle has less code with no polyfills, which serve for modern browsers.

That is <script type="module"> as a way to load ES modules. You can also load code with <script nomodule> for legacy browsers.

The rest of this article explains how to implement this technique and the bundle solution for webpack.

Concepts

How does it works? let's look at a example:

<!-- For modern browsers -->
<script type="module" data-original="main.mjs"></script>

<!-- For legacy browsers -->
<script nomodule data-original="main.js"></script>

In modern browsers, script with type="module" will be loaded and executed, and script with nomodule will be ignored.

And in legacy browsers, script with type="module" will be ignored because they can't recognize this attrbute, script with nomodule has no effect, it will be treated as usual.

Note: There's something you should know about <script type="module">.

  • It isn't executed until the document has been parsed, just like <script defer>.
  • Code is running in strict mode and top-level isn't window.

Warning: Safari 10 doesn’t support the nomodule attribute, but you can solve this by inlining a JavaScript snippet in your HTML (This has been fixed in Safari 11).

Implementation

I really appreciate it that @babel/preset-env provides a convenient config for esmodules.

babel for legacy:

{
  "presets": [
    [
      "@babel/preset-env", {
        "modules": false,
        "useBuiltIns": "entry",
        "targets": {
          "browsers": [
            "> 1%",
            "last 2 versions",
            "Firefox ESR"
          ]
        }
      }
    ]
  ]
}

babel for modern:

{
  "presets": [
    [
      "@babel/preset-env", {
        "modules": false,
        "useBuiltIns": false,
        "targets": {
          "esmodules": true
        }
      }
    ]
  ]
}

It's a bit complex for webpack to bundle two different JavaScript, there are some details you should know. So I wrote a esmodules-webpack-plugin to simplify configuration and bundle thest JavaScript just run webpack once.

Is it worth a try?

I think it's definitely worth a try, the size of polyfills that bundles by babel is more than what we think, and ES Modules code has an average reduction of 50% or even more, it all depends on your source code.

Besides, larger files not only take longer to download, but also take longer to parse and execute. So reduce file size is a efficient way to improve the performance of website.

Conclusion

<script type="module"> is really a fascinating technique that helps us shipping less code to users who use modern browsers and improve our website's performance.

However, there are also some limitations. for example, most module authors don’t publish ES6+ versions of their source code, a few browsers support <script type="module">, but will still download <script nomodule>(but won't execute).

Resources

A convenient webpack plugin for esmodules that I wrote:esmodules-webpack-plugin

查看原文

赞 0 收藏 0 评论 0

ansenhuang 评论了文章 · 2019-03-07

从实践中寻找webpack4最优配置

笔者最近在准备给fle-cli升级到webpack4版本,觉得有必要将探索过程的经验分享给大家,遂决定写这篇文章。(不知道fle-cli看这里

webpack4是大趋势,升级是必然的,那么为什么现在才升级?

原因有以下几个方面:

  • 刚发布的版本还不稳定,潜在风险大,而目前版本已更新到4.8.3,基本处于稳定;
  • webpack社区工具未完全跟上节奏,好多工具都得自己搞,劳心劳力(其实主要就是懒哈哈);
  • webpack本身及社区工具存在或多或少的问题,未经时间沉淀,维护成本高。

然而现在,笔者认为以上这些已经成熟,是时候来一波升级了。

前言

本文不会讲解webpack配置的每个细节点,因为这些官方文档都可以看到。笔者会挑一些难以理解的新概念、可能会碰到的问题,以及笔者总结下来的优化方案来分享,希望可以给大家带来一些帮助。

配置

mode

mode是webpack4新增的参数选项,它的值有3个:development、production、none,能够帮助我们加载一些默认配置,none即不加载默认配置。下面将对应的默认配置列出来供大家参考,以免重复配置。

development

注重提升代码构建速度和开发体验

module.exports = {
  cache: true,
  devtools: "eval",
  plugins: [
    new webpack.NamedModulesPlugin(),
    new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") })
  ]
}

prodution

提供代码优化,如压缩、作用域提升等

var UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJsPlugin(/* ... */),
    new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
    new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  ]
}

optimization

这个选项也是webpack4新增的,主要是用来自定义一些优化打包策略。

minimizer

在production模式,该配置会默认为我们压缩混淆代码,但这显然满足不了我们对于优化代码的诉求。下面笔者分享一套自身实践总结下来的配置及解释:


var UglifyJsPlugin = require('uglifyjs-webpack-plugin')
var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  optimization: {
    minimizer: [
      // 自定义js优化配置,将会覆盖默认配置
      new UglifyJsPlugin({
        exclude: /\.min\.js$/, // 过滤掉以".min.js"结尾的文件,我们认为这个后缀本身就是已经压缩好的代码,没必要进行二次压缩
        cache: true,
        parallel: true, // 开启并行压缩,充分利用cpu
        sourceMap: false,
        extractComments: false, // 移除注释
        uglifyOptions: {
          compress: {
            unused: true,
            warnings: false,
            drop_debugger: true
          },
          output: {
            comments: false
          }
        }
      }),
      // 用于优化css文件
      new OptimizeCssAssetsPlugin({
        assetNameRegExp: /\.css$/g,
        cssProcessorOptions: {
          safe: true,
          autoprefixer: { disable: true }, // 这里是个大坑,稍后会提到
          mergeLonghand: false,
          discardComments: {
            removeAll: true // 移除注释
          }
        },
        canPrint: true
      })
    ]
  }
}

UglifyJsPlugin这款插件相信大家也是经常用到,这里不再多说,这里的亮点是过滤掉本身已经是压缩的js文件,能够提升我们的编译效率以及避免二次混淆压缩而造成的未知bug。

OptimizeCssAssetsPlugin这款插件主要用来优化css文件的输出,默认使用cssnano,其优化策略主要包括:摈弃重复的样式定义、砍掉样式规则中多余的参数、移除不需要的浏览器前缀等,更多优化规则看这里。前文我们提到这里有个大坑,相信你已经察觉到了,没错,就是这货把我们通过autoprefixer加好了前缀给移除了。笔者查阅了许多资料,依旧没有找到满意的答案,没办法,只有硬着头皮去源码中找答案了,于是便有了这段配置autoprefixer: { disable: true },禁用掉cssnano对于浏览器前缀的处理。

runtimeChunk

分离出webpack编译运行时的代码,也就是我们先前称为manifest的代码块,好处是方便我们做文件的持久化缓存。它可以设置多种类型的值,具体可以看这里,其中single即将所有chunk的运行代码打包到一个文件中,multiple就是给每一个chunk的运行代码打包一个文件。

我们可以配合InlineManifestWebpackPlugin插件将运行代码直接插入html文件中,因为这段代码非常少,这样做可以避免一次请求的开销,但是新版插件的配置和之前有些不太一样,接下来详细讲解一下如何配置。

var HtmlWebpackPlugin = require('html-webpack-plugin')
var InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin')

module.exports = {
  entry: {
    app: 'src/index.js'
  },
  optimization: {
    runtimeChunk: 'single'
    // 等价于
    // runtimeChunk: {
    //   name: 'runtime'
    // }
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'fle-cli',
      filename: 'index.html',
      template: 'xxx',
      inject: true,
      chunks: ['runtime', 'app'], // 将runtime插入html中
      chunksSortMode: 'dependency',
      minify: {/* */}
    }),
    new InlineManifestWebpackPlugin('runtime')
  ]
}

这段配置会产生一个叫做runtime的代码块,和老版本不同的是,我们并不需要在html模版中添加<%= htmlWebpackPlugin.files.webpackManifest %>,只需将runtime加入chunks即可。这里有一个点要注意,InlineManifestWebpackPlugin插件的顺序一定要在HtmlWebpackPlugin之后,否则会导致编译失败。

splitChunks

终于要讲到重头戏了,也是笔者个人认为最难以理解的一个配置项。webpack4移除了CommonsChunkPlugin插件,取而代之的是splitChunks。

我们先来看下默认配置:

splitChunks: {
  chunks: "async",
  minSize: 30000,
  minChunks: 1,
  maxAsyncRequests: 5,
  maxInitialRequests: 3,
  automaticNameDelimiter: '~',
  name: true,
  cacheGroups: {
    vendors: {
      test: /[\\/]node_modules[\\/]/,
      priority: -10
    },
    default: {
      minChunks: 2,
      priority: -20,
      reuseExistingChunk: true
    }
  }
}

默认配置只会作用于异步加载的代码块,它限制了分离文件的最小体积,即30KB(注意这个体积是压缩之前的),这个是前提条件,然后它有两个分组:属于node_modules模块,或者被至少2个入口文件引用,它才会被打包成独立的文件。

为什么要限制最小体积呢?因为webpack认为小于30KB的代码块分离出来,还要额外消耗一次请求去加载它,成本太高,当然这个值也不是随便意淫出来的,而是经过大量的实践总结得到的,笔者个人认为这是一个非常好策略。

maxAsyncRequests(最大的异步请求数)和maxInitialRequests(最大的初始请求数)这两个参数则是为了限制代码块划分的过于细致,导致大量的文件请求。

但是只分离异步代码块显然满足不了我们的需求,因此接下来笔者分享一套相对来说比较优雅的分离打包配置:

splitChunks: {
  cacheGroups: {
    vendors: {
      test: /[\\/]node_modules[\\/]/,
      name: 'vendors',
      minSize: 30000,
      minChunks: 1,
      chunks: 'initial',
      priority: 1 // 该配置项是设置处理的优先级,数值越大越优先处理
    },
    commons: {
      test: /[\\/]src[\\/]common[\\/]/,
      name: 'commons',
      minSize: 30000,
      minChunks: 3,
      chunks: 'initial',
      priority: -1,
      reuseExistingChunk: true // 这个配置允许我们使用已经存在的代码块
    }
  }
}

首先是将node_modules的模块分离出来,这点就不再累述了。异步加载的模块将会继承默认配置,这里我们就不需要二次配置了。

第二点是分离出共享模块,笔者认为一个优雅的项目结构,其公共代码(或者称为可复用的代码)应该是放置于同一个根目录下的,基于这点我们可以将src/common中的公用代码提取出来。

当然你还可以有另外一种选择,将后缀为.js且使用次数超过3次的文件提取出来,但是笔者不建议这个做,因为这不利于持久化缓存,新增或删除文件都有可能影响到使用次数,从而导致原先的公共文件失效。

文末

原先还想着讲一下css插件部分的配置,限于篇幅,本文就不再进行讲解说明了,感兴趣的小哥哥小姐姐可以在这里翻看源码:webpack4-test

顺便在这里推荐一款好用的全局通用脚手架fle-cli:旨在帮助我们从复杂繁琐的编译配置中解放出来,全身心地投入业务开发中,提高开发效率;同时它也是真正意义上的全局脚手架,区别于市面上其他的全局脚手架,它不会在项目工程中生成各种编译配置文件,也不会给你安装一系列编译的依赖包,这意味着你的项目工程可以非常干净纯粹。

查看原文

ansenhuang 发布了文章 · 2019-01-22

The impacts of using index as key in React

Let's say there's a list that you want to show in React, and some developers may use index as key to avoid the warning of React, like this:

<ul>
  {list.map((item, index) =>
    <li key={index}>{item.name}</li>
  )}
</ul>

Sure, you can do that, but it's a bad idea. Let's see what official said:

We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state.

But how does the key work? Don't worry, this is what I want to talk about next.

Example

Let's start with a example to figure out what the difference between using index and using unique id.

Here is the example.

we render 2 different list initially and every item has a uncontrollable input. There are also some buttons on top which we can insert or delete items, each new item will be colored.

  • push: insert a item at the end of list
  • unshift: insert a item at the start of list
  • spliceInsert: insert a item at the middle of list

The left side represents list with index-key, the right side represents list with unique-key.

dom confused
text confused

As we can see from pictures, there seems to be something wrong on the left side. It got confused about which item belonged to which dom.

Analyze

It will reuse the dom that already exist if their key are the same, that's the role of key. For more detail, here is the source code.

For the sake of understanding, I'll explain it in a case.

An array of length 5, I want to insert an item in 3rd position. when it comes to index of [0, 1], it's okay to reuse the existing doms. But when it comes to index of 2, it also reuses the existing dom because of same key. Besides, their state is different so the children of dom[2] will be updated. Next, index of 3 reuses dom[4] and so on.

It may cause terrible performance if list is long enough. If you insert a item at the start of list, it'll insert a dom at the end and update children all of item. But with unique id, it just insert a dom at the start.

Conclusion

It's a bad idea to use the array index since it doesn't uniquely identify your elements. In cases where the array is sorted or an element is added to the beginning of the array, the index will be changed even though the element representing that index may be the same. This results in unnecessary renders.

If you have to use index as key, please make sure you only operate the last item.

查看原文

赞 0 收藏 0 评论 0

ansenhuang 发布了文章 · 2019-01-15

React's setState is not asynchronous!

We always hear that React's setState is asynchronous, and I've been convinced of this until I read the source code of React.

Today, I want to tell you that setState is absolutely synchronous, no asynchronous except you open the Concurrent Mode of React, but it's still unstable now, so what I talking about is based on Sync Mode of React.

I divide setState into 2 parts, triggered by React event and the others. A little confused? It doesn't matter, keep up with me and then you will understand.

Hint: This is my first article written in English, there maybe some mistakes of grammar, please forgive me.

SyntheticEvent

At the beginging, I want to explain the concept of SyntheticEvent in React. It owes to SyntheticEvent that we can merge updates and render them only once, we also call it as batch updates. If you aren't familiar with SyntheticEvent, here is the reference.

Update Processes

However, what's the connection between setState and SyntheticEvent? here we have a example:

import React from 'react';

export default class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      clickCount: 0
    };
  }

  handleClickCount = () => {
    this.setState(state => ({
      clickCount: state.clickCount + 1
    }));
    console.log('# state.clickCount', this.state.clickCount);
  }

  render () {
    console.log('# render start');

    return (
      <div>
        <p>{this.state.clickCount}</p>
        <button onClick={this.handleClickCount}>Increment</button>
      </div>
    );
  }
}

When button is clicked, console will show these infomation:

# state.clickCount 0
# render start

setState was already executed, but the state wasn't updated, Why?

I guess many developers are familiar with this example, and the conclusion of React's setState is asynchronous is also based on this.
It seems to be asynchronous, but it's actually synchronous, let me show you the real secret about setState.

When we click button, the first executed function isn't handleClickCount but the TopLevelEvent which was captured by document, then it marks isBatchingUpdates which is one of global variable in React as true # code line, fn which contains events will be executed, and performSyncWork will start render phase.

Let's refer to processes of setState, when it encounters requestWork#code line, if isBatchingUpdates is true, it will be returned before perfomSyncWork. That's why setState is synchronous but state wasn't updated immediately.

I simplified the whole processes as following:

let isBatchingUpdates = false;

function setState() {
  if (isBatchingUpdates) {
    return;
  }

  performSyncWork();
}

function dispatEvent(e) {
  isBatchingUpdates = true;

  fn(); // execute React events based on e.target

  isBatchingUpdates = false;
  if (!isBatchingUpdates) {
    performSyncWork(); // start render phase
  }
}

document.addEventListener('click', dispatEvent, false);

As we can see above, there's not any asynchronous code. In the event listener, it haven't executes performSyncWork so state is still old.

The order of execution:

  • trigger document event
  • set isBatchingUpdates as true
  • execute synthetic event
  • execute setState
  • returned before performSyncWork(because isBatchingUpdates is true)
  • log the old state
  • execute performSyncWork in dispatchEvent

From what I just said, we can draw a conclusion: React's setState is synchronous, but the order of execution is different between SyntheticEvent and the others.

Examples

Note that we know the principle of setState, let's make a little change of the example.

I added some code following:

...
handleClickCountLater = () => {
  setTimeout(this.handleClickCount, 0);
}
...
// render
...
<button onClick={this.handleClickCountLater}>Increment Later</button>
...

You can guess what will happen before I show you the answer.

The console will show following infomation when you click this button:

# render start
# state.clickCount 1

In the document listener, it pushes setTimeout to WebAPIs. In next event loop, setState will be executed, but isBatchingUpdates is false at this time, so it continues to execute performSyncWork. The result is the same if you replace setTimeout with Promise.resolve.

let's add a native event to button element.

// constructor
...
this.btnRef = React.createRef();
...

componentDidMount() {
  this.btnRef.current.addEventListener('click', this.handleClickCount, false);
}

// render
...
<button ref={this.btnRef}>Increment Ref</button>
...

Due to btnRef's event isn't controlled by SyntheticEvent, we will get these infomation at console:

# state.clickCount 1
# render start

Conclusion

All in all, setState is synchronous, but the order of execution is different between SyntheticEvent and the others. In the SyntheticEvent, React will collect all the updates and then update them once, which we call batch updates.

If you want to update state based on the old state, I strongly recommend Functional setState, because it will help you avoid some bugs. To learn more about Functional setState, here is an article maybe help you.

查看原文

赞 0 收藏 0 评论 0

ansenhuang 评论了文章 · 2019-01-11

禁止蒙层底部页面跟随滚动

场景概述

弹窗是一种常见的交互方式,而蒙层是弹窗必不可少的元素,用于隔断页面与弹窗区块,暂时阻断页面的交互。但是,在蒙层元素中滑动的时候,滑到内容的尽头时,再继续滑动,蒙层底部的页面会开始滚动,显然这不是我们想要的效果,因此需要阻止这种行为。

那么,如何阻止呢?请看以下分析:

方案分析

方案一

  • 打开蒙层时,给body添加样式:
overflow: hidden;
height: 100%;

在某些机型下,你可能还需要给根节点添加样式:

overflow: hidden;
  • 关闭蒙层时,移除以上样式。

优点:
简单方便,只需添加css样式,没有复杂的逻辑。

缺点:
兼容性不好,适用于pc,移动端就尴尬了。
部分安卓机型以及safari中,无法无法阻止底部页面滚动。

如果需要应用于移动端,那么你可能需要方案二。

方案二

就是利用移动端的touch事件,来阻止默认行为(这里可以理解为页面滚动就是默认行为)。

// node为蒙层容器dom节点
node.addEventListener('touchstart', e => {
  e.preventDefault()
}, false)

简单粗暴,滚动时底部页面也无法动弹了。假如你的蒙层内容不会有滚动条,那么上述方法prefect。

但是,最怕空气突然安静,假如蒙层内容有滚动条的话,那么它再也无法动弹了。因此我们需要写一些js逻辑来判断要不要阻止默认行为,复杂程度明显增加。

具体思路:判定蒙层内容是否滚动到尽头,是则阻止默认行为,反之任它横行。


Tip:这里我发现了一个小技巧,可以省略不少代码。在一次滑动中,若蒙层内容可以滚动,则蒙层内容滚动,过程中即使蒙层内容已滚至尽头,只要不松手(可以理解为touchend事件触发前),继续滑动时页面内容不会滚动,此时若松手再继续滚动,则页面内容会滚动。利用这一个小技巧,我们可以精简优化我们的代码逻辑。

示例代码如下:

<body>
  <div class="page">
    <!-- 这里多添加一些,直至出现滚动条 -->
    <p>页面</p>
    <p>页面</p>
    <button class="btn">打开蒙层</button>
    <p>页面</p>
  </div>
  <div class="container">
    <div class="layer"></div>
    <div class="content">
      <!-- 这里多添加一些,直至出现滚动条 -->
      <p>蒙层</p>
      <p>蒙层</p>
      <p>蒙层</p>
    </div>
  </div>
</body>
body {
  margin: 0;
  padding: 20px;
}

.btn {
  border: none;
  outline: none;
  font-size: inherit;
  border-radius: 4px;
  padding: 1em;
  width: 100%;
  margin: 1em 0;
  color: #fff;
  background-color: #ff5777;
}

.container {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 1001;
  display: none;
}

.layer {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 1;
  background-color: rgba(0, 0, 0, .3);
}

.content {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 50%;
  z-index: 2;
  background-color: #f6f6f6;
  overflow-y: auto;
}
const btnNode = document.querySelector('.btn')
const containerNode = document.querySelector('.container')
const layerNode = document.querySelector('.layer')
const contentNode = document.querySelector('.content')
let startY = 0 // 记录开始滑动的坐标,用于判断滑动方向
let status = 0 // 0:未开始,1:已开始,2:滑动中

// 打开蒙层
btnNode.addEventListener('click', () => {
  containerNode.style.display = 'block'
}, false)

// 蒙层部分始终阻止默认行为
layerNode.addEventListener('touchstart', e => {
  e.preventDefault()
}, false)

// 核心部分
contentNode.addEventListener('touchstart', e => {
  status = 1
  startY = e.targetTouches[0].pageY
}, false)

contentNode.addEventListener('touchmove', e => {
  // 判定一次就够了
  if (status !== 1) return

  status = 2

  let t = e.target || e.srcElement
  let py = e.targetTouches[0].pageY
  let ch = t.clientHeight // 内容可视高度
  let sh = t.scrollHeight // 内容滚动高度
  let st = t.scrollTop // 当前滚动高度

  // 已经到头部尽头了还要向上滑动,阻止它
  if (st === 0 && startY < py) {
    e.preventDefault()
  }

  // 已经到低部尽头了还要向下滑动,阻止它
  if ((st === sh - ch) && startY > py) {
    e.preventDefault()
  }
}, false)

contentNode.addEventListener('touchend', e => {
  status = 0
}, false)

问题虽然是解决了,但是回头来看,复杂程度和代码量明显增加了一个梯度。
本着简单方便的原则,我们是不是还可以探索其他的方案呢?

既然touch事件判定比较复杂,何不跳出这个框框,另辟蹊径,探索更加合适的方案。
于是,便有了我们的方案三。

方案三

来讲讲我的思路,既然我们要阻止页面滚动,那么何不将其固定在视窗(即position: fixed),这样它就无法滚动了,当蒙层关闭时再释放。
当然还有一些细节要考虑,将页面固定视窗后,内容会回头最顶端,这里我们需要记录一下,同步top值。

示例代码:

let bodyEl = document.body
let top = 0

function stopBodyScroll (isFixed) {
  if (isFixed) {
    top = window.scrollY

    bodyEl.style.position = 'fixed'
    bodyEl.style.top = -top + 'px'
  } else {
    bodyEl.style.position = ''
    bodyEl.style.top = ''

    window.scrollTo(0, top) // 回到原先的top
  }
}

思考总结

  • 若应用场景是pc,推荐方案一,真的是不要太方便
  • 若应用场景是h5,你可以采用方案二,但是我建议你采用方案三
  • 若应用场景是全平台,那么方案三你不容错过

本文到这里也即将结束了,在这里我强烈推荐一下方案三,原因在于简单、方便、兼容性好,一次封装,永久受用。

查看原文

ansenhuang 评论了文章 · 2018-11-22

禁止蒙层底部页面跟随滚动

场景概述

弹窗是一种常见的交互方式,而蒙层是弹窗必不可少的元素,用于隔断页面与弹窗区块,暂时阻断页面的交互。但是,在蒙层元素中滑动的时候,滑到内容的尽头时,再继续滑动,蒙层底部的页面会开始滚动,显然这不是我们想要的效果,因此需要阻止这种行为。

那么,如何阻止呢?请看以下分析:

方案分析

方案一

  • 打开蒙层时,给body添加样式:
overflow: hidden;
height: 100%;

在某些机型下,你可能还需要给根节点添加样式:

overflow: hidden;
  • 关闭蒙层时,移除以上样式。

优点:
简单方便,只需添加css样式,没有复杂的逻辑。

缺点:
兼容性不好,适用于pc,移动端就尴尬了。
部分安卓机型以及safari中,无法无法阻止底部页面滚动。

如果需要应用于移动端,那么你可能需要方案二。

方案二

就是利用移动端的touch事件,来阻止默认行为(这里可以理解为页面滚动就是默认行为)。

// node为蒙层容器dom节点
node.addEventListener('touchstart', e => {
  e.preventDefault()
}, false)

简单粗暴,滚动时底部页面也无法动弹了。假如你的蒙层内容不会有滚动条,那么上述方法prefect。

但是,最怕空气突然安静,假如蒙层内容有滚动条的话,那么它再也无法动弹了。因此我们需要写一些js逻辑来判断要不要阻止默认行为,复杂程度明显增加。

具体思路:判定蒙层内容是否滚动到尽头,是则阻止默认行为,反之任它横行。


Tip:这里我发现了一个小技巧,可以省略不少代码。在一次滑动中,若蒙层内容可以滚动,则蒙层内容滚动,过程中即使蒙层内容已滚至尽头,只要不松手(可以理解为touchend事件触发前),继续滑动时页面内容不会滚动,此时若松手再继续滚动,则页面内容会滚动。利用这一个小技巧,我们可以精简优化我们的代码逻辑。

示例代码如下:

<body>
  <div class="page">
    <!-- 这里多添加一些,直至出现滚动条 -->
    <p>页面</p>
    <p>页面</p>
    <button class="btn">打开蒙层</button>
    <p>页面</p>
  </div>
  <div class="container">
    <div class="layer"></div>
    <div class="content">
      <!-- 这里多添加一些,直至出现滚动条 -->
      <p>蒙层</p>
      <p>蒙层</p>
      <p>蒙层</p>
    </div>
  </div>
</body>
body {
  margin: 0;
  padding: 20px;
}

.btn {
  border: none;
  outline: none;
  font-size: inherit;
  border-radius: 4px;
  padding: 1em;
  width: 100%;
  margin: 1em 0;
  color: #fff;
  background-color: #ff5777;
}

.container {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 1001;
  display: none;
}

.layer {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 1;
  background-color: rgba(0, 0, 0, .3);
}

.content {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 50%;
  z-index: 2;
  background-color: #f6f6f6;
  overflow-y: auto;
}
const btnNode = document.querySelector('.btn')
const containerNode = document.querySelector('.container')
const layerNode = document.querySelector('.layer')
const contentNode = document.querySelector('.content')
let startY = 0 // 记录开始滑动的坐标,用于判断滑动方向
let status = 0 // 0:未开始,1:已开始,2:滑动中

// 打开蒙层
btnNode.addEventListener('click', () => {
  containerNode.style.display = 'block'
}, false)

// 蒙层部分始终阻止默认行为
layerNode.addEventListener('touchstart', e => {
  e.preventDefault()
}, false)

// 核心部分
contentNode.addEventListener('touchstart', e => {
  status = 1
  startY = e.targetTouches[0].pageY
}, false)

contentNode.addEventListener('touchmove', e => {
  // 判定一次就够了
  if (status !== 1) return

  status = 2

  let t = e.target || e.srcElement
  let py = e.targetTouches[0].pageY
  let ch = t.clientHeight // 内容可视高度
  let sh = t.scrollHeight // 内容滚动高度
  let st = t.scrollTop // 当前滚动高度

  // 已经到头部尽头了还要向上滑动,阻止它
  if (st === 0 && startY < py) {
    e.preventDefault()
  }

  // 已经到低部尽头了还要向下滑动,阻止它
  if ((st === sh - ch) && startY > py) {
    e.preventDefault()
  }
}, false)

contentNode.addEventListener('touchend', e => {
  status = 0
}, false)

问题虽然是解决了,但是回头来看,复杂程度和代码量明显增加了一个梯度。
本着简单方便的原则,我们是不是还可以探索其他的方案呢?

既然touch事件判定比较复杂,何不跳出这个框框,另辟蹊径,探索更加合适的方案。
于是,便有了我们的方案三。

方案三

来讲讲我的思路,既然我们要阻止页面滚动,那么何不将其固定在视窗(即position: fixed),这样它就无法滚动了,当蒙层关闭时再释放。
当然还有一些细节要考虑,将页面固定视窗后,内容会回头最顶端,这里我们需要记录一下,同步top值。

示例代码:

let bodyEl = document.body
let top = 0

function stopBodyScroll (isFixed) {
  if (isFixed) {
    top = window.scrollY

    bodyEl.style.position = 'fixed'
    bodyEl.style.top = -top + 'px'
  } else {
    bodyEl.style.position = ''
    bodyEl.style.top = ''

    window.scrollTo(0, top) // 回到原先的top
  }
}

思考总结

  • 若应用场景是pc,推荐方案一,真的是不要太方便
  • 若应用场景是h5,你可以采用方案二,但是我建议你采用方案三
  • 若应用场景是全平台,那么方案三你不容错过

本文到这里也即将结束了,在这里我强烈推荐一下方案三,原因在于简单、方便、兼容性好,一次封装,永久受用。

查看原文

认证与成就

  • 获得 128 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-09-20
个人主页被 637 人浏览