BingqiChen

BingqiChen 查看完整档案

杭州编辑浙江大学宁波理工学院  |  电子信息工程 编辑杭州推宝科技有限公司  |  前端工程师 编辑 bingqichen.me 编辑
编辑

Everything should be perfect.

个人动态

BingqiChen 收藏了文章 · 10月14日

框架设计:RN 端的框架如何设计与落地

著作权归作者所有。商业转载请联系 Scott 获得授权,非商业转载请注明出处[务必保留全文,勿做删减]。

线下越重,线上需要越轻,这个轻指的是轻便轻巧和简洁易用,B2B2C 生鲜领域在线下是如此之重,那么在交易场景线上化的过程中,端的移动化就势在必行,试想一下,让菜市场摊位老板人手一台笔记本点开网页选购支付,让采购销售抱着电脑去拜访客户,一边聊蔬菜行情,一边打开笔记本进行记录,有没有一种回到世纪初的感觉。

产品的移动化,这将是我们展开这篇文章的背景,我们会先了解小菜的产品托管在哪些端上,然后感受这些端带来的挑战,最后是我们聚焦如何做移动端的框架封装,包括必要的基建部分。

小菜大前端的端有哪些

小菜早期围绕着蔬菜销地以客户集单批发的模式摸爬滚打几年,从上游的蔬菜供应商到下游批发市场的摊位老板,在这个长长的链路中,我们诞生了这样几款线上产品来服务于不同的人群和场景,之前文章中也有介绍,这里再汇总一下,共 9 款 App:

  • 宋小菜 服务于销地批发老板的下单工具
  • 宋小福 服务于小菜内部销售团队的 CRM 销售管理与客户管理工具
  • 宋小仓 连接司机-物流-采购-销售的蔬菜在途位置监控工具
  • 采秘    服务于小菜内部采购团队的蔬菜品类采购工具
  • 麦大蔬 服务于上游蔬菜供应商的大宗农产品交易平台
  • 宋大仓 服务于上游囤货配资的进出库管理平台
  • 云掌柜 服务于产区加工厂进销存的移动 Saas
  • 卖大蔬 服务于产销行情与货源泛用户的内容小程序
  • 行情宝 服务于产销两地的内部行情采集和预测工具

前 7 款 App 都是基于 ReactNative 开发的 iOS/Android App,最后两个是微信小程序,它们涵盖了公司几乎所有的协同场景和工作流。

多端带来的技术挑战

1. 【物理现状】移动端的碎片化

古典互联网时代,因为要兼容 IE678 而痛苦不堪,Hack 黑魔法经验基本代表前端水平,如今互联网早已移动化,我们理想中的移动端开发,看上去是可以大胆使用新语法特性,只需要做好尺寸兼容就好了,但事实并非如此,不仅在移动端的浏览器不是如此,在移动端开发 RN App 也是如此,这是我们某一款 App 一段时间内,所收集上来的手机厂商分布:

可以发现 Android 的碎片化非常严重,每一个厂商下面有不同时期推出的不同型号的手机,这些手机有着不同版本的操作系统,不同的分辨率和用电策略,不同的后台进程管理方式和用户权限,要让一款 App 在哪怕头部 40% 的手机上兼容,都是一件艰难的事情,这个客观物理现状叠加下面的社区现状,App 质量保证这件事情会变得雪上加霜。

2. 【社区现状】技术框架的不稳定性

回到本文的开头,我们在长链路的 B2B 生鲜场景中,为了更快更轻,开发出了 7 款 App,而且将来随着业务场景的拓展会诞生更多独立 App 甚至是集大成的 App,所以技术选型不太可能选择原生的 Java/Object-C 开发,尤其对于创业公司,7 款 App 得需要多少名原生开发工程师才能搞定,高频繁重的业务变化又怎样靠堆人来保证?

想清楚这些,一开始我们就调研 ReactNative,并最终全部从原生切换到了 RN。通过跑过来的这 4 年来看,使用 RN 为公司节约了大量的人力成本同时,也尽可能的满足到了几乎所有的需要快速迭代的业务场景,又快又轻,成为宋小菜大前端团队做事的一个典型特征。

但换一个角度看,就是带来的问题。又快又轻的背后是 RN 版本的飞速迭代,截止到目前,也就是 2019 年 4 月份,RN 还没有推出一个官方的正式的长期维护的稳定版本,什么意思?就是 RN 目前依然处在不稳定的研发周期内,我们依然站在刀尖上起舞,用不稳定的 RN 版本试图开发稳定的应用。四年走来,我们在 RN 的框架里,多少次面对旧版本局限性和新版本不稳定性都进退不得,旧版本的 Bug 可能会在新版本中修复,新版本引进则会带来新版本自己的问题。

除了 RN 自身版本,还有第二个问题,围绕着 RN 有很多业界优秀的组件,但这些社区组件甚至官方组件,都不一定能及时跟进最新的 RN 版本,同时还能兼容到较老的 RN 版本,所以 RN 升级导致的组件不兼容性,会引发你 Fork 修改组件的冲动,但这样会带来额外的开发成本和版本维护成本,取舍会成为版本升降的终极问题。

在国内开发,还有第三个问题,就是中文文档缺乏,社区资源匮乏,参考文献陈旧,可拿来主义的开源工程方案甚至社区线上线下会议分享都很缺乏,一个不小心就会踩坑,这就是 RN 社区的现状,我们在刀尖浪花上独步,App 选型背后的技术栈稳定性则成为悬在头上的一把铡刀,你不知道什么时候会咔嚓一声。

3. 【人才现状】人员能力的长短不齐

我们知道有一个词叫做主观能动性,表示没有条件创造条件也可以上。这个词的主体就是人,聊完移动端设备现状和社区现状后,我们来聊聊人的问题。RN 在国内真正开始普及使用,是从 2015 年开始,也就意味着,到 2019 年,一个 RN 工程师最多也就只有 4 年的工作经验,而 RN 的 “Learn once, write anywhere” 也刺激着一切 Care 人员开支, Care 产品研发投入性价比的公司纷纷跳水研究 RN,争抢 RN 人才,RN 是前端中的移动前端,前端有多抢手,那么 RN 工程师就比它还要抢手。

这就导致基本上 RN 工程师很难靠外部招聘,只能靠内部培养。这也是小菜前端的成长历程,我们有  2 名资深 RN 工程师,一个是从服务端 Java,一个是从原生 Android 开发转过来的。如果 RN 人手不足,产品支持的力度和速度就一定会遇到瓶颈,这就是我们曾经面临的问题,就是人才现状,外招数量不足,内培速度有限,RN 工程师的数量和能力就时不时成为公司业务扩张的瓶颈。

4. 【公司现状】高密集业务的交付质量

作为工程师,我们有很强的自尊心和不容挑战的代码洁癖,但在一个创业公司里面,甚至大公司的一个创业团队里面,我们需要对接一些关键的业务节点,冲刺一些特定的时间窗口,并且要及时响应多变的业务,和业务背后多变的产品形态,这都会带来非常密集的需求队列。

这些密集的需求队列对我们的代码质量有非常高的挑战,一个组件用 5 分钟思考如何抽象和用 50 分钟思考,实现后的稳定性、兼容性都是不同的。如何保证产品按期交付上线,会是摆在我们面前一个非常关键的命题,而这个难题之外,还有一个更难的命题等着我们,那就是如何保证交付不延期的同时,还能保证交付质量。

要知道,如果一个项目代码赶的太毛糙,后期维护起来的成本会是巨大的,甚至只能用更高的成本重构重写。本质上,再次重构就一定是公司在为早期的猛冲买单,为这些技术债买单,如何不去买单或者如何用最小的成本买单,这跟我们早期的业务密集程度,交付周期,质量把控有很大的关系。

综上,移动端碎片化所带来的兼容难度,RN 框架的局限性,版本间差异带来的不稳定性,技术社区资源的匮乏和前端团队技术能力掣肘,再叠加上高密度的业务排期,让前端开发这个本来很酷的事情,变得晴雨不定。

这些避不开的现实,是绕不过去的坎儿,必须通过人才储备和技术基建来缓解,接下来我们进入到本文的重点 - RN 框架的封装。

RN 的 App 工程如何架构

RN 的 App 工程骨架,全部抽象完毕,再搭配上组件化,就可以称为一个基于 ReactNative 定制的 App 框架了,而 RN 涉及到原生层面的技术细节太多,我们暂不做讨论,只专注在工程与业务的封装上。

我们在构建 RN App 工程时需要关注这几个关键要素:

  • 配置管理
  • 静态文件管理
  • 网络请求
  • 组件管理
  • 路由管理
  • 数据缓存
  • App 的热更新
  • 数据搜集
  • 应用状态管理

1. 配置管理

配置管理是指可以灵活合理的管理 App 的内部环境,主要包括:

  • App 本身的一些配置
  • 所使用三方插件的配置

我们在构建工程时尽量将所有的配置抽象统一放置在一个地方,这样便于查找和修改。但是由于大多数配置都统一放在同一个地方,那么就难免有部分文件要使用某个配置时其引用路径比较长,比如:

import { pluginAConfig } from '../../../../../config'

这样就造成了阅读性很差且代码不美观,因此我们可以使用 Facebook 的 fbjs 模块提供的一个功能 providesModule :

//config.js
/**
 * config for all
 * @providesModule config 
 * 使用 providesModule 将 config 暴露出去
 **/
import pluginAConfig from './plugin_a_config'

export default {
    pluginAConfig
}

// 然后在其他文件中调用
// A.js
import { pluginAConfig } from 'config'

这样就能很方便地在 App 的任意一处使用 config 了,但是我们要避免滥用 providesMoudle ,因为使用了 providesMoudle 进行声明的模块的源码,想要在编辑器中使用跳转到定义的方式去查看比较困难,不利于团队多人合作。

2. 静态资源

静态资源泛指会被多次调用的图片或 icon,我们一般在 RN 使用图片时是直接引用的:

import { Image } from 'react-native'

render(){
  return (
    <Image source={{uri: './logo.png'}} />
  )
}

当图片需要在多处使用时,我们可能会将这些可能会被反复使用的图片统一管理到 assets 文件夹中,统一管理和使用,但是当需要使用图片资源的文件嵌套较深时,引用图片就变得麻烦:

render(){
  return (
    <Image source={{uri: '../../../../assets/logo.png'}} />
  )
}

这个问题与配置管理的问题一样,可以首先将图片资源按照类型进行分类,比如 assets 文件夹下有 button/icon/img/splash/svg 等,每一个类型的结构如下:

- icon/
 - asset/
 - index.js

其中 asset 文件夹保存我们的图片资源,在 index.js 中对图片进行引用并暴露为模块:

// index.js
export default {
   IconAlarmClockOrange: require('./asset/icon_alarm_clock_orange.png'),
   IconAvatarBlue: require('./asset/icon_avatar_blue.png'),
   IconArrowLeftBlue: require('./asset/icon_arrow_left_blue.png'),
   IconArrowUpGreen: require('./asset/icon_arrow_up_green.png')
}

然后再在 assets 文件夹下编辑 index.js ,将所有的图片资源作为 assets 模块暴露出去,为了避免和其他模块冲突你可以修改模块名为 xxAssets

// assets/index.js
/**
 * @providesModule myAssets
 **/
 import Splash from './splash'
 import Icon from './icon'
 import Img from './img'
 import Btn from './button'
 import Svg from './svg'

 export {
   Splash,
   Icon,
   Img,
   Btn,
   Svg
 }

// A.js
import { Icon } from 'myAssets'

render(){
  return (
    <Image source={Icon.IconAlarmClockOrange} />
  )
}

这样,我们就能很方便地将分散在项目各处的图片资源统一到一个地方进行管理了,使用起来也非常方便。

3. 网络请求

网络请求这块,react-native 使用 whatwg-fetch,我们也可以选择其他的三方包如 axios 来做网络请求。但有时候我们会在开发中遇到一个问题,那就是我们明明已经在代码里已经修改了 cookie, 但是每次请求可能还是会带上之前的 cookie 从而造成一些困扰,所以这里推荐一个实用的组件 Networking :

import { NativeModules } from 'react-native'
const { Networking } = NativeModules

// 手动清除已缓存 Cookie,这样就能解决上述的问题了
Networking.clearCookies(callBack)

当然,Networking 的功能不止于此,还有很多其他有趣的功能可以发掘,可以直接用它来包装自己的网络请求工具,还支持 abort ,可以参考 源码 来具体把玩。

4. 组件化

使用 RN 开发 App 本身效率就比较高,如果想要继续进阶就要考虑组件化开发,一旦涉及到组件化开发,就不可避免地会涉及到组件管理的问题,这里的组件管理比较宽泛,它实际上应该指的是:

  • 组件规范
  • 组件类型划分
  • 组件开发标准

组件规范指的是 UI 设计规范,我们可以与设计同学交流规定好一套特定的规范,然后将通用的样式属性(如主题颜色,按钮轮廓,返回按键,Tab 基础样式等)定义出来,便于所有的组件让开发者在开发时使用,而不是开发者各自为政在开发时重复写样式文件,这里推荐一个比较好用的用于样式定义的三方插件 react-native-extended-stylesheet ,我们可以使用这个插件定义我们的通用属性:

// mystyle
import { PixelRatio, Dimensions } from 'react-native'
import EStyleSheet from 'react-native-extended-stylesheet'

const { width, height } = Dimensions.get('window')

const globals = {
  /** build color **/
  $Primary: '#aa66ff',
  $Secondary: '#77aa33',
  $slimLine: 1 / PixelRatio.get(),
  /** dimensions **/
  $windowWidth: width,
  $windowHeight: height
}

EStyleSheet.build(globals)

module.exports = {
  ...EStyleSheet,
  create: styleObject => EStyleSheet.create(styleObject),
  build: (obj) => {
    if (!obj) {
      return
    }
    EStyleSheet.build(_.assign(obj, globals))
  }
}

// view.js
import MyStyleSheet from 'mystyle'

const s = MyStyleSheet.create({
  container: {
    backgroundColor: '$Secondary',
    width: '$windowWidth'
  }
})

render....

这样,我们就能在开发的任意插件或者 App 中直接使用这些基础属性,当某些属性需要修改时只需要更新 mystyle 组件即可,另外还可以衍生出主题切换等功能,使得开发更加灵活。

关于组件类型我们会抛开三方组件以及原生组件,因为一旦涉及到这两者,需要写的东西就太多了,我们将组件按使用范围分为通用组件和业务组件两大类。

首先什么是业务组件?即我们在开发某个业务产品常用到的组件,这个组件绑定了与业务相关的一些特殊属性,除了这个业务开发以外,其他地方都不适用,但是在开发这个业务时多个页面会频繁地使用到,所以我们有必要将其抽象出来,方便使用。

什么是通用组件?即可以在 App 范围内使用甚至于跨 App 使用的组件,这里可以对这个类别进行细分,我们将能跨 App 使用的组件上传到了自己的搭建的私有 npm  仓库,方便我们的 App 开发者使用,同时,具有 App 自己特色的组件则放到工程中统一管理,同样适用 providesModules 暴露出去。

制定一整套组件开发标准的是很重要的,因为很多组件开发可能是多人维护的,有一套既定的规范就可以降低维护成本,组件使用的说明文档的完善也同样重要。

5. 路由管理

开发 App 就不可避免地会遇到如何管理页面以及处理页面跳转等问题,也就是路由管理问题,自从 Facebook 取消了 RN 本身自带的 Navigator 以后,许多依赖于这个组件的开发者不得不将目光投向百花齐放的社区三方组件,FB 随后推荐大家使用的是 react-community 推出的 react-navigation ,现在这个路由组件已经独立出来了。我们在开发时就是使用的这个组件作为路由管理组件,只不过是在其基础上做了一些定制 ,使得使用更加简单,部分跳转动作更加符合我们的产品场景,推荐大家使用这个组件。当然,除去这个组件还有很多其他的组件可供选择:

路由管理作为整个 App 的骨架,它是这几个部分中最重要的一部分,合理地定制和使用路由管理可以极大地简化我们的开发复杂度。

6. 数据缓存

一般情况下需要缓存的数据基本上就可能是我们会在 App 很多地方都会使用到的全局数据,如用户信息,App 设置(非应用层面的设置)等,RN 提供一个 AsyncStorage 存储引擎,通常的使用方式是对这个数据引擎进行包装后暴露出符合我们要求的读写接口。这里推荐另外一种使用方式:

既然需要缓存的数据可能是会在 App 很多地方使用到的全局数据,那么我们可以将这些全局数据使用 redux 来进行管理,而利器 redux-persist 则能让我们很优雅地读写我们的缓存数据。

同时,如果对 react-navigation 进行合理的定制,接管其路由管理,那么我们还能实现保存用户退出 App 之前最后浏览的页面的状态,用户在下次打开 App 依然可以从之前浏览的地方继续使用 App,当然,这个功能要谨慎使用!

7. 热更新

App 的版本更新,RN 除了传统的 App 更新外还有一个热更新的可选项(传统 App 更新也有热更新,其原理就不太一样了),社区大多数人都推荐使用 codepush 来进行热更新,至于其后端解决方案 貌似已经有了一个 code-push-server ,我们是使用自己的热更新方案,其原理就是在不更新原生代码的基础上更新 JS 代码和静态资源文件。

8. 数据搜集

搜集的 App 使用数据(包括异常数据)并对此分析,根据分析来定位问题是保证 App 质量的有效手段之一。你可以选择自己搭建一套数据搜集服务,包括客户端 SDK 和服务端搜集服务,或者选择市场上已有的工具,目前较为成熟的收据搜集工具比较多,如友盟,mixpanel, countly 等等,在此不作赘述。

9. 应用状态管理

React只是视图层的解决方案,对于复杂应用,需要涉及状态之间的共享、各层级组件之间的通信、多接口之间调用的同步等等,就需要进行应用状态管理,Facebook最早提出了Flux架构思想,后来社区又涌现了Redux、Mobx等很多种模式。经过调研比较,我们选择了Redux进行应用状态管理,Redux的核心概念主要是通过Store、Action、Reducer、Dispatch实现单向数据流动,具体概念请参考官方文档。Redux通过middleware机制,可以对Redux进行各种能力增强,这个增强其实是在action分发至任务处理reducer之前做一些额外的工作,dispatch发布的action先依次传递给中间件,然后最终到达reducer,所以使用middleware机制我们可以拓展很多能力,例如我们使用了状态持久化插件redux-persist,状态记录和重播插件redux-logger,而异步操作插件我们经历了两轮技术选型redux-thunk和redux-saga。

支持函数action的redux-thunk通过简单的几行代码使得只处理plain object的action支持异步操作。

if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
}
return next(action);

Redux-thunk的实现非常简单,使用也非常灵活。我们可以在action中处理各种异步操作,也可以做任何事情,但是它的缺点是它缺乏对异步的直接处理,异步操作分散在各个action 中,而同步接口等操作依赖使用者自己的实现。

于是我们进而选择了支持generator的redux-saga。Redux-saga通过一个类似于独立线程的方式管理你的应用程序中的副作用,这意味着你可以通过普通的redux action开始、暂停或者取消saga线程。Redux-saga使用ES6的generator来管理异步流,使得业务逻辑的读写和测试变得更简单。在我们最新的架构中,我们其实使用的是蚂蚁金服开源的dva-core。之所以选用dva-core,主要是因为dva-core整合了redux和redux-saga,并且使开发者可以通过一个命名的model文件集中管理一个业务逻辑的state,通过定义的effects管理副作用操作,通过定义reducers管理其他处理函数。一个完整的model大概是这样的:

export default {
    namespace: 'order',
  effects: {...},
  reducers: {...},
  subscription: {...}
}

最后,关于应用状态管理,还有一个话题可以讨论,就是状态的不可变性immutable。在redux中状态是不可变的,每个reducer都会产生新的不可变状态。那么这个不可变性是否需要不可变js库(比如immutable.js)的支持呢?简单来说,immutable.js可以带来计算效率和存储效率的提升,但是它需要使用库支持的数据类型,所以如果从头构建一个应用,可以选择。如果是对于一个已有的复杂应用进行重构,那就需要综合考虑一下了。

小结

总结一下,一个 RN App 架构应该要保证 App 的运行稳定以及开发的便捷。运行稳定这一方面,除了从 JS 层面(如单元测试,JS 错误上报等)保证之外,很大程度上还要依赖于原生层面的处理,所以团队里面要有同学的精力可以投在原生研究上面,至于开发便捷,我们尽量将复杂重要或者简单繁琐的操作在构建工程时就做掉,这样也可以大幅度提高我们的开发效率,降低开发者之间的合作沟通成本。

:::info
Scott 近两年无论是面试还是线下线上的技术分享,遇到许许多多前端同学,由于团队原因,个人原因,职业成长,技术方向,甚至家庭等等原因,在理想国与现实之间,在放弃与坚守之间,摇摆不停,心酸硬扛,大家可以找我聊聊南聊聊北,对工程师的宿命有更多的了解,有更多的看见与听见,Scott 微信: codingdream,也可以来 关注 Scott 语雀跟进最新动态,本文未经许可不许转载,获得许可请联系 Scott,否则在公众号上直接转载,尤其是裁剪内容后转载,我都会直接进行投诉处理。
:::

2.png
1.png

查看原文

BingqiChen 评论了文章 · 2018-06-22

谈谈 dva 和 mobx 的使用感受

在使用 react 的时候或多或少会接触到状态管理,从开始学 react 到现在也挺久了,也用过几种状态管理工具,今天谈一谈这些工具的区别吧。

dva

经朋友推荐开始接触 dva ,从 1.x 版本开始使用,一直看着它发展到前不久的 2.x,我也基于这个工具开发了一套项目模版,它简化了 redux 的使用,并且在封装了 redux-saga 和 react-router,同时还可以包含 dva-loading 插件获取 loading 状态等。

在 redux 时代,当我需要新增一种跨页面全局数据的时候,我需要去项目的 reducers 目录定义一下这种数据命名和初始值,然后在 constans 目录中为更改这项数据的操作定义一种唯一的操作类型(type),再去 actions 目录定义一些方法,这些方法最后会得到更改后的数据和操作类型(type),最后再回到 reducers 中根据这个操作类型(type)把数据整合到 reducer 中…可以看到,我在编写 redux 这部分代码的时候需要频繁在 actions 、 constants 、 reducers 这几个目录间切换。

而使用 dva 就可以免除这些困扰了,我只需要一个 model 中就可以完成所有操作:

// app全局性状态管理
import * as appApis from '../services/app'; // 异步请求接口

export default {
  namespace: 'app',

  state: {
    channels: [],
    show: true
  },

  reducers: {
    getChannelsAndGamesSuccess(state, { channels, games }) {
      return { ...state, channels, games };
    },
    changeShow(state, { show }) {
      return { ...state, show };
    }
  },

  effects: { // 异步
    * getChannelsAndGames(_, { call, put }) {
      const res = yield call(appApis.getChannelsAndGames);
      yield put({
        type: 'getChannelsAndGamesSuccess',
        channels: res.channels
      });
    }
  },
    
  subscriptions: { // 订阅
    setup({dispatch, history}) {
      history.listen(location => {
        if (location.pathname == '/') {
          dispatch({
            type: 'getChannelsAndGames'
          });
        }
      });
    }    
  }
};

这便是一个 model 对象,state 定义数据、effects 中执行异步请求、触发 action 在 reducers 中改变数据,一气呵成!

此外它还含有其他特性,比如:subscription(订阅),可以在这里订阅 history 路由变化,进入根路径执行 getChannelsAndGames 获取数据,而不需要在 react 的生命周期中做这些事;用上 dva-loading 插件,在更改数据的过程中还会自动设置 loading 状态,这在异步请求中非常有用!

mobx

既然 dva 这么好用,为什么还要使用 mobx 呢?还不是为了折腾?,用了才能知道两者的优劣,同样的基于 mobx 我也创建了一个项目模版

在使用 dva 的时候,但凡遇到异步请求的时候都需要先定义一个 effects ,请求完成后再触发一个 action 去修改数据,于是,强迫症作怪,这两者的命名总是让我感觉难受和啰嗦,你可以看到我都是定义为 getXxxgetXxxSuccess

action 是修改 state 的唯一途径,是的,所有的状态管理库都是这样的,但是 mobx 通过一些工具函数解决了这一问题:

// app全局性状态管理
import { observable, action, runInAction } from 'mobx';
import * as appApis from '../services/app';

export default class AppStore {
  @observable show = true;
  @observable list = [];

  @action toggleShow = () => { this.show = !this.show; }
  @action getData = async (params) => {
    try {
      const res = await appApis.getTopicsList(params);
      // await 之后,再次修改状态需要动作: 
      runInAction(() => {
        this.list = res.data;
      });
    } catch (e) {
      console.error(e);
    }
  }
}

/**
 * -----------------------------------------------------------------
 */
// app全局性状态管理
import { observable, action } from 'mobx';
import { asyncAction } from "mobx-utils"
import * as appApis from '../services/app';

export default class AppStore {
  @observable show = true;
  @observable list = [];

  @action toggleShow = () => { this.show = !this.show; }
  @asyncAction * getData(params) { // <- 注意*号,这是一个 generator 函数!
    try {
      const res = yield appApis.getTopicsList(params); // 用 yield 代替 await
      this.list = res.data;
    } catch (e) {
      console.error(e);
    }
  }
}

以上是我最喜欢的两种写法,分别借助了 runInActionasyncAction 这两个工具函数,当然,还有其他方法可以参考。

总结

不管是 redux 、 dva ,还是 mobx ,每种技术的出现都是为了带给我们更好的开发体验、更高的开发效率,也伴随着不一样的学习成本,虽然 mobx 可以治好我的强迫症,但是也反逼我去学习了修饰器;虽然看起来 redux 是最繁琐的,但是它可能是对新手最友好的,参考资料也是最多的,也能够一步步让我知道状态是如何改变的,再者,如果我没使用过 redux ,未必能体会到后两者带给我怎样的便利之处。

查看原文

BingqiChen 发布了文章 · 2018-04-19

[译]浅显易懂的 this 取值规则

翻译自文章The Simple Rules to ‘this’ in Javascript

确定什么是 this 并非难事。总的来说,通过查找函数被调用时的位置(和方法)就可以决定。遵循以下规则,按优先级排列。

规则

  1. 通过 new 关键字调用构造函数,函数内的 this 是一个全新的对象。

    function ConstructorExample() {
        console.log(this);
        this.value = 10;
        console.log(this);
    }
    new ConstructorExample();
    // -> {}
    // -> { value: 10 }
  2. 通过 applycallbind 调用一个函数,函数内的 this 就是传入的参数。

    function fn() {
        console.log(this);
    }
    var obj = {
        value: 5
    };
    var boundFn = fn.bind(obj);
    boundFn();     // -> { value: 5 }
    fn.call(obj);  // -> { value: 5 }
    fn.apply(obj); // -> { value: 5 }
  3. 如果一个函数作为对象的方法调用,即使用 . 符号调用该函数, this 是调用该函数的对象。换句话说,当 . 处于被调用函数的左边,则 this 就是左边的对象。

    var obj = {
        value: 5,
        printThis: function() {
            console.log(this);
        }
    };
    obj.printThis(); // -> { value: 5, printThis: ƒ }
  4. 如果函数作为普通函数调用,意味着调用方式不符合以上任意一种, this 就是全局对象。在浏览器中就是 window

    function fn() {
        console.log(this);
    }
    // If called in browser:
    fn(); // -> Window {stop: ƒ, open: ƒ, alert: ƒ, ...}

    *这个规则可以类比于规则3——不同之处在于这个函数自动挂载到了 window 对象上,所以可以这么理解,当我们调用 fn() 时其实调用的事 window.fn() ,所以 this 就是 window

    console.log(fn === window.fn); // -> true
  5. 如果符合上述多个规则,则越前面的规则会决定 this 的值。
  6. 如果函数是一个 ES2015 箭头函数,会忽略上述所有规则, this 设置为它被创建时的上下文。为了找到 this 的值,需要找到函数被创建时的环境中 this 的值。

    const obj = {
        value: 'abc',
        createArrowFn: function() {
            return () => console.log(this);
        }
    };
    const arrowFn = obj.createArrowFn();
    arrowFn(); // -> { value: 'abc', createArrowFn: ƒ }

    我们返回去看规则3,当我们调用 obj.createArrowFn() 时, createArrowFn 中的 this 就是 obj 对象,我们用 . 符号调用。如果我们在全局中创建一个箭头函数, this 就是 window

应用规则

下面在几个例子中应用一下我们的规则。试一下通过两种不同的方式调用函数时 this 的值。

找到应用的规则

var obj = {
    value: 'hi',
    printThis: function() {
        console.log(this);
    }
};
var print = obj.printThis;
obj.printThis(); // -> {value: "hi", printThis: ƒ}
print(); // -> Window {stop: ƒ, open: ƒ, alert: ƒ, ...}

obj.printThis() 很显然应用的是规则3——使用 . 符号。 print() 应用了规则4,在调用 print() 时,我们没有使用 newbind/call/apply. 符号,所以这里的 this 是全局对象 window

多重规则应用

如上文提到,当应用多个规则时,优先应用前面的规则。

var obj1 = {
    value: 'hi',
    print: function() {
        console.log(this);
    },
};
var obj2 = { value: 17 };

如果规则2和3同时应用,规则2优先。

obj1.print.call(obj2); // -> { value: 17 }

如果规则1和3同时应用,规则1优先。

new obj1.print(); // -> {}

关于工具库

一些 JavaScript 库有时候会在函数中主动绑定它认为最有用的内容到 this 上。比如在 JQuery中,在触发事件时 DOM 元素被绑定到了 this 上。在使用工具库时发现取值不符合上述规则时,请查看库文档。很可能使用了 bind 语法。

该文章首发于我的个人站点

查看原文

赞 0 收藏 3 评论 0

BingqiChen 发布了文章 · 2018-04-18

[译]浅显易懂的 this 取值规则

翻译自文章The Simple Rules to ‘this’ in Javascript

确定什么是 this 并非难事。总的来说,通过查找函数被调用时的位置(和方法)就可以决定。遵循以下规则,按优先级排列。

规则

  1. 通过 new 关键字调用构造函数,函数内的 this 是一个全新的对象。

    function ConstructorExample() {
        console.log(this);
        this.value = 10;
        console.log(this);
    }
    new ConstructorExample();
    // -> {}
    // -> { value: 10 }
  2. 通过 applycallbind 调用一个函数,函数内的 this 就是传入的参数。

    function fn() {
        console.log(this);
    }
    var obj = {
        value: 5
    };
    var boundFn = fn.bind(obj);
    boundFn();     // -> { value: 5 }
    fn.call(obj);  // -> { value: 5 }
    fn.apply(obj); // -> { value: 5 }
  3. 如果一个函数作为对象的方法调用,即使用 . 符号调用该函数, this 是调用该函数的对象。换句话说,当 . 处于被调用函数的左边,则 this 就是左边的对象。

    var obj = {
        value: 5,
        printThis: function() {
            console.log(this);
        }
    };
    obj.printThis(); // -> { value: 5, printThis: ƒ }
  4. 如果函数作为普通函数调用,意味着调用方式不符合以上任意一种, this 就是全局对象。在浏览器中就是 window

    function fn() {
        console.log(this);
    }
    // If called in browser:
    fn(); // -> Window {stop: ƒ, open: ƒ, alert: ƒ, ...}

    *这个规则可以类比于规则3——不同之处在于这个函数自动挂载到了 window 对象上,所以可以这么理解,当我们调用 fn() 时其实调用的事 window.fn() ,所以 this 就是 window

    console.log(fn === window.fn); // -> true
  5. 如果符合上述多个规则,则越前面的规则会决定 this 的值。
  6. 如果函数是一个 ES2015 箭头函数,会忽略上述所有规则, this 设置为它被创建时的上下文。为了找到 this 的值,需要找到函数被创建时的环境中 this 的值。

    const obj = {
        value: 'abc',
        createArrowFn: function() {
            return () => console.log(this);
        }
    };
    const arrowFn = obj.createArrowFn();
    arrowFn(); // -> { value: 'abc', createArrowFn: ƒ }

    我们返回去看规则3,当我们调用 obj.createArrowFn() 时, createArrowFn 中的 this 就是 obj 对象,我们用 . 符号调用。如果我们在全局中创建一个箭头函数, this 就是 window

应用规则

下面在几个例子中应用一下我们的规则。试一下通过两种不同的方式调用函数时 this 的值。

找到应用的规则

var obj = {
    value: 'hi',
    printThis: function() {
        console.log(this);
    }
};
var print = obj.printThis;
obj.printThis(); // -> {value: "hi", printThis: ƒ}
print(); // -> Window {stop: ƒ, open: ƒ, alert: ƒ, ...}

obj.printThis() 很显然应用的是规则3——使用 . 符号。 print() 应用了规则4,在调用 print() 时,我们没有使用 newbind/call/apply. 符号,所以这里的 this 是全局对象 window

多重规则应用

如上文提到,当应用多个规则时,优先应用前面的规则。

var obj1 = {
    value: 'hi',
    print: function() {
        console.log(this);
    },
};
var obj2 = { value: 17 };

如果规则2和3同时应用,规则2优先。

obj1.print.call(obj2); // -> { value: 17 }

如果规则1和3同时应用,规则1优先。

new obj1.print(); // -> {}

关于工具库

一些 JavaScript 库有时候会在函数中主动绑定它认为最有用的内容到 this 上。比如在 JQuery中,在触发事件时 DOM 元素被绑定到了 this 上。在使用工具库时发现取值不符合上述规则时,请查看库文档。很可能使用了 bind 语法。

该文章首发于我的个人站点

查看原文

赞 0 收藏 3 评论 0

BingqiChen 发布了文章 · 2018-04-15

webpack@v4升级踩坑

之前看到各大公众号都在狂推 webpack 新版发布的相关内容,之前就尝试了升级,由于部分插件的原因,未能成功,现在想必过了这么久已经可以了,今天就来试一下在我的项目中升级会遇到哪些坑。

查阅更新日志

在安装更新之前,先大致浏览了一下更新日志,对大部分用户来说迁移上需要注意的应该就是这些点:

  • 在命令行界面运行打包指令需要安装 webpack-cli
  • 打包需要指定打包模式 production or development ,在不同模式下会添加不同的默认配置, webpack.DefinePlugin 插件的 process.env.NODE_ENV 的值不需要再定义,将根据模式自动添加;
  • 不再需要在 plugin 中设置 new webpack.optimize.UglifyJsPlugin ,只需要在配置中设置开关即可,并且 production 模式自动开启,可以通过 optimization.minimizer 指定其他压缩库;
  • 删除了 CommonsChunkPlugin ,功能已迁移至 optimization.splitChunks ,optimization.runtimeChunk

迁移

  1. 安装最新的 webpackwebpack-cliwebpack-dev-server
  2. 为开发中和发布分别配置 mode ,删除 webpack.DefinePlugin 配置,并且去掉 package.json 中启动脚本的 NODE_ENV 区别环境变量定义;
  3. 去掉 new webpack.optimize.UglifyJsPluginModuleConcatenationPlugin 配置。

爬坑

别慌

  1. 在这些配置好之后我遇到的第一个问题就是打包时 extract-text-webpack-plugin 插件炸了!这里提供了这里有两种解决方案:

    • 方法一:安装指定 extract-text-webpack-plugin 版本 @next
    • 方法二:使用 mini-css-extract-plugin 替代。

      如果使用方法二注意在发布打包时需要指定 css 压缩库配置,并且需要同时写入 js 压缩库,因为你一旦指定了 optimization.minimizer 就会弃用内置的代码压缩:

      /* webpack.config.js */
      const MiniCssExtractPlugin = require('mini-css-extract-plugin');
      
      module.exports = () => {
        const config = {
          module: {
            rules: [
              {
                test: /\.css$/,
                use: [
                  MiniCssExtractPlugin.loader,
                  'css-loader?importLoaders=1',
                  'postcss-loader'
                ]
              },
              {
                test: /\.less$/,
                use: [
                  MiniCssExtractPlugin.loader,
                  'css-loader?importLoaders=1',
                  'postcss-loader',
                  'less-loader'
                ]
              }
            ]
          },
          resolve: {
            extensions: ['.js', '.jsx', '.less']
          }
        };
        
        if (process.env.NODE_ENV === 'development') {
          config.module.rules[0].use = [
            'css-hot-loader',
            MiniCssExtractPlugin.loader,
            'css-loader?importLoaders=1',
            'postcss-loader'
          ];
          config.module.rules[1].use = [
            'css-hot-loader',
            MiniCssExtractPlugin.loader,
            'css-loader?importLoaders=1',
            'postcss-loader',
            {
              loader: 'less-loader',
              options: {
                modifyVars: theme
              }
            }
          ];
        }
      
        return config;
      };
      
      /* webpack.config.prod.js */
      const merge = require('webpack-merge');
      const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
      const MiniCssExtractPlugin = require('mini-css-extract-plugin');
      const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
      const webpackBaseConfig = require('./webpack.config')();
      
      module.exports = merge(webpackBaseConfig, {
        mode: 'production',
        optimization: {
          minimizer: [
            new UglifyJsPlugin({
              cache: true,
              parallel: true,
              uglifyOptions: {
                compress: {
                  warnings: false,
                  drop_debugger: true,
                  drop_console: false
                }
              }
            }),
            new OptimizeCSSAssetsPlugin({})
          ]
        },
        plugins: [
          new MiniCssExtractPlugin({
            filename: 'css/[name].css'
          })
        ]
      });
  2. happypack 炸了,小场面,升级就好 @5.0.0-beta.3happypackextract-text-webpack-plugin 搭配使用更佳,mini-css-extract-plugin 未测试)。
  3. webpack-browser-plugin 炸了,小场面,弃用就好,然后在 devServer 中配置 openopenPage
  4. 上面的配置中可以看到我使用判断语句 process.env.NODE_ENV === 'development' 在开发配置中加入了 css-hot-loader ,但是这里实际上是获取到的是 undefined ,咦?这是什么鬼?查阅更新日志是怎么说的:

    process.env.NODE_ENV are set to production or development (only in built code, not in config)

    意思就是说我们在使用的工程项目代码中会获取到这个变量,但是打包配置中使用这个变量还是获取不到的,我也实际验证了这个结果,so,我在 package.json 的开发启动脚本中还是加上了 NODE_ENV='development'

最后

总体来说现在的升级时机已经成熟,大多需要用到的功能和插件都有平滑的升级或替代方案,建议在开始升级前安装最新发布的插件版本,也可以参考下我的项目配置react-with-mobx-template

还有对插件的一些 API 也做了一些更改,如果你是插件开发者也可以尝试发布新的插件版本,我在使用自己的版本号提取插件webpack-version-plugin时发现 compiler.plugin 已经被提示过气了, webpack@v4 使用最新的 compiler.hooks.emit.tap 触发事件,嗯,最后的这部分广告真硬!

23333

该文章首发于我的个人站点

查看原文

赞 16 收藏 16 评论 0

BingqiChen 发布了文章 · 2018-04-06

从 Wireshark 看一次请求的过程

从大学有网络课程起就知道有三次握手这回事,但对其中到底发生了什么一直懵懵懂懂,今天打算借助 Wireshark 这一著名的网络数据包分析软件重现一下握手过程。

TCP 头部格式释义

字段对应关系及释义:

TCP头部格式

三次握手

关于 Wireshark

Wireshark

在 Wireshark 中左边实线(图中21 - 28、133 - 134、427 - 428)连起来的一段可以视为为同一次会话内发生的各个阶段,不过图中的是简单顺利的一次会话过程,没有失败重传、分片传输等其他情况。

另外:

  • 每一条中的中括号的为 ACK 为通信控制位,后面的 ACK确认号 表示接收方当前接收到的字节数;
  • SYN、FIN 控制位会占用一个确认号。

文中7001为服务器所在端口号。

三次握手之一

第21个网络包:客户端发送第一个网络包,在头部写入发送方、接收方端口号(用来找到目标套接字),设置客户端初始序号为0, SYN 控制位为1,等待服务器确认,由于此时客户端还未接收过网络包,所以 ACK 控制位为0。

三次握手之二

第22个网络包:服务器收到网络包,同样返回一个响应包,在TCP头部写入发送方、接收方端口号(此时对服务器来说是发送方)、服务端初始序号0, SYN 控制位1,另外还要设置 ACK 控制位为1,表示已收到有效网络包;除此之外可以注意到 ACK号 也被置为1了,就是上文提到的 SYN 控制位占用的一个确认号。

三次握手之三

第23个网络包:客户端收到网络包,发现 SYN 控制位为1,表示连接成功,同时发送一个 ACK 控制位设置为1的网络包给服务器,告知服务器刚才的响应包已收到,这里可以发现序号变为1,那是因为服务器返回的响应包中确认了客户端发送的 SYN 控制位为1,所以序号需要 +1;在服务器收到后三次握手就全部完成了,后面就可以开始收发数据了。

三次握手示意图

后面的[TCP Window Update]是用来窗口更新的,这里不做阐述。

收发数据

客户端发起请求

第25个网络包:客户端发起了一个 HTTP 页面请求,TCP详细信息如下:

No.25

可以看到这个网络包已经不单单是控制信息的传输,开始包含数据,并且数据占用621字节,同时 Wireshark 已经机智在计算下一个客户端发送的包序号了, 621…...

服务器发送确认包

然后服务器告知客户端已收到该请求,响应第26个网络包:

No.26

ACK号 = 前一次的 ACK号 + 本次收到的数据字节数 = 1 + 620 = 621;

序号保持不变,因为本次只是发送控制信息,并没有发送数据。

Tips:在 WireShark 选中某个网络包,如上图No.26,结果No.25前面出现了一个对勾,可以看作是No.26对No.25的消息确认。

服务器发送内容响应请求

在第27个网络包中,服务器针对HTTP请求返回页面内容:

No.27

可以看到这个网络包的数据大小为4152字节,这之后理论上在客户端应该会返回一个 ACK号 为4153的确认包。

客户端发送确认包

No.28

在第28个网络包中,序号就如前面 Wireshark 计算的等于621,ACK号的计算和第26个网络包相似,不再赘述。

关闭连接

在结束最后一个网络包的传输后,再过一段时间(上图中大概5秒),如没有数据往来Web服务器就会关闭连接,至于为什么要延时,大概是为了短时间内如果有后续数据交换,可以减少重新创建套接字建立连接的开销吧。

关闭连接握手之一

服务器生成包含断连控制信息 FIN 的TCP头部发送给客户端。

关闭连接握手之二

客户端告知服务器已收到断连信号 FIN , 因为收到的 FIN 占用一个确认号,所以回复的 ACK号 = 4153 + 1 = 4154。

关闭连接握手之三

客户端在读取所有缓冲区的数据后,也会向服务器发送一个 FIN 为1的网络包。

关闭连接握手之四

服务器同理返回 ACK号 确认收到 FIN 控制信息。

这就是断连操作的四次握手。

关闭连接示意图

附测试数据

测试数据下载链接,过滤规则 tcp.port == 7001

该文章首发于我的个人站点

查看原文

赞 3 收藏 3 评论 2

BingqiChen 发布了文章 · 2018-04-03

canvas 高仿 Apple Watch 表盘

不知道大家童年时候有没有在手上画手表的经历,恰好最近在看 canvas ,于是就诞生了这个高仿表盘。

童年画手表

实现过程

我这里参照了 Apple Watch 中的这个表盘:

Apple Watch 表盘

绘制表盘背景

<div class="wrap">
  <canvas id="canvas" width="400" height="400"></canvas>
</div>

<script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    
    ctx.translate(200, 200); // 移动坐标系中心点
    // 绘制黑色底盘
    ctx.beginPath();
    ctx.moveTo(-100, -80);
    ctx.quadraticCurveTo(-100, -100, -80, -100);
    ctx.lineTo(80, -100);
    ctx.quadraticCurveTo(100, -100, 100, -80);
    ctx.lineTo(100, 80)
    ctx.quadraticCurveTo(100, 100, 80, 100);
    ctx.lineTo(-80, 100);
    ctx.quadraticCurveTo(-100, 100, -100, 80);
    ctx.closePath();
    ctx.fillStyle = 'black';
    ctx.fill();
</script>
  • 这里用到了一个变换属性 translate ,跟 css 中的属性相似,把后面绘制过程中的坐标系进行了平移,方便计算;
  • 为了绘制圆角这里用了二次贝塞尔曲线,当然使用 ctx.arc 圆弧也可以,如下示意图右上角的点 cp 就是该位置贝塞尔曲线的控制点。

表盘示意图

表盘黑色背景

绘制日期和表盘刻度

绘制日期文字:

const now = moment();

ctx.textAlign = 'left';
ctx.fillStyle = '#ce4c50';
ctx.font = '11px "Helvetica Neue"';
ctx.textBaseline = 'middle';
ctx.fillText(now.format('D'), 15, 0);
ctx.fillStyle = 'white';
ctx.fillText(now.format('ddd'), 33, 0);

绘制刻度:

ctx.fillStyle = 'white';
ctx.font = '20px "Helvetica Neue"';
ctx.strokeStyle = 'white';
ctx.textAlign = 'center';

// 绘制圆盘刻度点和数字
for (let index = 60; index > 0; index -= 1) {
    if (index % 5 === 0) {
        ctx.lineWidth = 3
        ctx.fillText(index / 5, 0, -70);
    } else {
        ctx.lineWidth = 1;
    }
    ctx.beginPath();
    ctx.moveTo(0, -90);
    ctx.lineTo(0, -85);
    ctx.stroke();
    ctx.rotate(- Math.PI * 2 * (1 / 60));
}
  • 这里用到了 rotate 属性,即绕中心点旋转,需要刻画 60 个刻度,一周的弧度为 2 Math.PI ,每次旋转 1 / 60 ,每 5* 格添加小时文字并加粗刻度线;
  • 这里还有个小时文字角度的问题没有找到好的解决方法。

绘制刻度

绘制表盘指针

对指针的绘制,首先以原点为中心绘制一个圆,对延伸出来的指针思考了两种绘制方法:

指针示意图

第一种:以 Y轴左半边 为例,点 (-2, 0) 为起始点,以 cp1cp2 为控制点,(-1, -12) 为终点绘制三次贝塞尔曲线;

第二种:以 Y轴右半边 为例,直接从点 (1, -1) 绘制直线到 (1, -12)

 // 时针
const hour = new Path2D();
hour.arc(0, 0, 2, 0, Math.PI * 2);

// hour.moveTo(-2, 0);
// hour.bezierCurveTo(-2, -2, 0, -1, -1, -12);
hour.moveTo(-1, -1);
hour.lineTo(-1, -12)

hour.lineTo(-2, -13);
hour.lineTo(-2, -45);
hour.arc(0, -47, 2, -Math.PI, 0);
hour.lineTo(2, -13);

// hour.lineTo(1, -12);
// hour.bezierCurveTo(0, 1, 2, -2, 2, 0);
hour.lineTo(1, -12);
hour.lineTo(1, -1);

我把两种都实现了一遍发现效果都差不多,大概是我绘制的图形不够大,看不出区别。

时针

接下去需要把指针指向对应的时间,以 16点20分 为例,我们计算时针需要旋转的角度:

const h = 16 % 12; // 表盘上只有12大格
const m = 20;

const hAngle = h / 12; // 这里只计算旋转角度占一圈的比例,每小时 1/12
const mAngle = (m / 60) * (1 / 12); // 不满1小时的部分,还需要乘上分钟数在1小时中的比例

const angle = Math.PI * 2 * (hAngle + mAngle); // 最终需要旋转的角度

ctx.rotate(angle);
ctx.fill(hour);
ctx.stroke(hour);

时针旋转

接下去在描述分针的时候我们发现只是末端部分稍微长一点,我们可以选择沿用时针的部分代码,写成这样:

// 分针
const minute = new Path2D(hour); // 沿用代码
minute.moveTo(-2, -45);
minute.lineTo(-2, -82);
minute.arc(0, -84, 2, -Math.PI, 0);
minute.lineTo(2, -13);

const minute = new Path2D(); // 不沿用代码
minute.arc(0, 0, 2, 0, Math.PI * 2);
minute.moveTo(-1, -1);
minute.lineTo(-1, -12)
minute.lineTo(-2, -13);
minute.lineTo(-2, -82);
minute.arc(0, -84, 2, -Math.PI, 0);
minute.lineTo(2, -13);
minute.lineTo(1, -12);
minute.lineTo(1, -1);

值得注意的一点是:我们在绘制完时针后,如果接下去直接计算角度绘制分针,上下文会以上次旋转的角度为基础,叠加旋转效果,所以绘制分针之前需要还原到初始坐标系,我用的是 saverestore 函数保存状态/还原状态。

分针、秒针计算绘制过程雷同不再赘述。

实现动画

实现动画过程只需要擦除已绘制的内容,再次绘制并重置变换效果即可,重绘使用 setIntervalrequestAnimationFrame 都可以:

function draw() {
    ctx.resetTransform(); // 重新绘图前清除变换效果
    ctx.clearRect(0, 0, 400, 400); // 清除内容
    ...
    window.requestAnimationFrame(draw);
}
window.requestAnimationFrame(draw);

成果图

完整实现

详见jsfiddle

该文章首发于我的博客https://blog.bingqichen.me/

查看原文

赞 2 收藏 4 评论 0

BingqiChen 发布了文章 · 2018-04-03

react 实现页面代码分割、按需加载

虽然一直有做 react 相关的优化,按需加载、dll 分离、服务端渲染,但是从来没有从路由代码分割这一块入手过,昨天在本地开发时没有测试成功,今天又搞了下,已经部署到线上环境了,今天就这个记录一下。

修改配置

  • 开发环境:webpack@v3react-router@v4
  • 安装依赖:

    $ yarn add babel-plugin-syntax-dynamic-import -dev
  • 修改 .babelrc 文件:在 plugins 中添加 "syntax-dynamic-import"

改造项目代码

  • 安装依赖:

    $ yarn add react-loadable
  • 根据 react-loadable文档提示,我们需要提供一个载入新页面时的 Loading 组件,同时对加载和超时状态进行区别提示:

    import React from 'react';
    import { Icon } from 'antd';
    
    const Loading = ({ pastDelay, timedOut, error }) => {
      if (pastDelay) {
        return <div><Icon type="loading" /></div>;
      } else if (timedOut) {
        return <div>Taking a long time...</div>;
      } else if (error) {
        return <div>Error!</div>;
      }
      return null;
    };
  • 更改页面组件导入方法:

    import React from 'react';
    import Loadable from 'react-loadable';
    import { Route, Switch } from 'react-router-dom';
    
    const Home = Loadable({
      loader: () => import('../Home'),
      loading: Loading,
      timeout: 10000
    });
    const EditArticle = Loadable({
      loader: () => import('../EditArticle'),
      loading: Loading,
      timeout: 10000
    });
    
    ...
    
    <Switch>
        <Route exact path="/home" component={Home} />
        <Route path="/editarticle" component={EditArticle} />
    </Switch>

    然后打包结果就会分离出各页面代码:

    打包结果

    在页面中我们只需要载入入口文件 app.js ,其他脚本在访问到对应页面时都会经由这个文件载入。

验证结果

在将静态资源上传到 cdn 之后,在页面中加载 app.cssapp.js ,运行之后访问各个页面就会依次加载对应脚本,结果如图:

运行结果

可以看到,在访问第一个页面时加载的页面脚本经 gzip 压缩后仅有 21.8KB !!!当然这与页面复杂度也有关,但是相较于加载全部脚本,已经是大幅减少了,这种优化对访问目标性很强的用户感受起来尤为明显。

这么做的另一个好处就是当我们只更改项目中某些页面的业务代码时,其他页面的代码依然是不变的,所以这个时候其他页面用的是客户端缓存,从另一个层面又做了一次优化。

Tips

该文章首发于我的博客https://blog.bingqichen.me/

查看原文

赞 3 收藏 4 评论 5

BingqiChen 发布了文章 · 2018-03-30

移动端字体大小调节器实现

最近在一个微信H5项目中需要用到字体大小调节器,就看了一下QQ中的功能,就做了一个相似的,方法也不止一种,而且都不难。

img1

方法1

html

<div class="wrap">
    <div class="line"></div>
    <ul class="font-adjust">
        <li data-size="14px" class="selected"></li>
        <li data-size="16px"></li>
        <li data-size="18px"></li>
        <li data-size="20px"></li>
        <li data-size="22px"></li>
    </ul>
</div>
<article style="font-size: 14px">
    圣诞已过<br>再加上看过各个大牌出的辣眼睛狗年限定以后<br>我以为未来的几个月应该是没有什么东西<br>能骗到我的钱了<br>直到...看到了ysl的春季限定...<br>我知道<br>钱包又要完。蛋 。了。
</article>

style

<style type="text/css">
* {
    margin: 0;
    padding: 0;
    border: none;
    box-sizing: border-box;
}
.wrap {
    position: relative;
    padding: 0 50px;
    border: 1px solid #000;
}
.line {
    width: calc(100% - 100px);
    height: 1px;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    background-color: #ccc;
}
.font-adjust {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 50px;
}
.font-adjust li {
    position: relative;
    list-style: none;
    width: 1px;
    height: 10px;
    background-color: #ccc;
}
.font-adjust li::after {
    content: '';
    display: block;
    position: absolute;
    width: 20px;
    height: 20px;
    top: 50%;
    left: 50%;
    border-radius: 50%;
    transform: translate(-50%, -50%);
    background-color: transparent;
}
.font-adjust li.selected::after {
    background-color: #fff;
    border: 1px solid #eee;
    box-shadow: 0 1px 1px 0 #999;
}
article {
    text-align: center;
}
</style>

我们得到这样的页面:

img2

javascript

最简单的单击目标点可以设置对应字体大小:

$('.font-adjust li').on('click', function(e) {
    $('article').attr('style', `font-size: ${$(this).attr('data-size')}`); // 设置字体大小
    $('.font-adjust li').removeClass('selected');
    $(this).addClass('selected');
})

在QQ的字体设置中,只要点击起始点在调节区域,之后在整个窗口左右滑动也可以达到调节字体的目的,我这里用的是 touchmove 事件,利用它返回的位置信息,判断当前距离哪个点的水平位置最近,就使用哪个字体大小,代码如下:

var fontModel = ['14px', '16px', '18px', '20px', '22px']; // 可供选择的字体序列
var current = 0; // 当前使用的字体在序列中的位置下标
var fontModelsPos = $('.font-adjust li').map(function (index) { // 获得每个标记点的位置 x
    return $('.font-adjust li').eq(index)[0].offsetLeft;
})

$('.font-adjust').on('touchmove', function (e) {
    e.preventDefault();
    var min = {
        i: 0,
        dis: Math.abs(fontModelsPos[0] - e.changedTouches[0].clientX)
    };
    for(var i = 1; i < 5; i++){ // 获得最近标记点
        var dis = Math.abs(fontModelsPos[i] - e.changedTouches[0].clientX); // 计算触控点和各标记点的距离
        if (dis < min.dis) { // 找出最近的那个
            min = { i: i, dis: dis }
        }
    }
    if (min.i != current) { // 字体大小改变
        current = min.i;
        $('.font-adjust li').removeClass('selected');
        $('.font-adjust li').eq(min.i).addClass('selected');
        $('article').attr('style', `font-size: ${fontModel[min.i]}`)
    }
})

效果:

img3

方法2

emmmmm,后来隐约记得有个元素可以充当这个来调节器来用,是它,是它,就是它:inputinput 中有一个属性 type="range" ,使用它可以更方便的完成上述功能。

html

<div class="wrap">
    <input type="range" min="0" max="4" step="1" value="0" id="adjust_font">
    <!-- 以下模拟横线和定位点 -->
    <div class="line"></div>
    <ul class="range-simulate">
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
    </ul>
</div>

这里还是保留了上面的部分代码(5个定位点和1条横线),因为 input 是没有这些点的,横线可以通过更改默认滑轨的样式成为一条细线,但是当滑块在两端时,横线的顶点并不在滑块中央,不介意的话问题也不大。

如下的透视图展示的就是顶点在滑块正中央:

img4

style

<style type="text/css">
* {
    margin: 0;
    padding: 0;
    border: none;
    box-sizing: border-box;
}
.wrap {
    position: relative;
    padding: 10px 50px;
    border: 1px solid #000;
}
input[type=range] {
    -webkit-appearance: none;
    width: 100%;
    background-color: transparent;
}
input[type=range]::-webkit-slider-runnable-track { // 滑轨
    height: 20px;
}
input[type=range]::-webkit-slider-thumb { // 滑块
    -webkit-appearance: none;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background-color: #fff;
    border: 1px solid #eee;
    box-shadow: 0 1px 1px 0 #999;
}
input[type=range]:focus {
    outline: none;
}
.line {
    width: calc(100% - 120px);
    margin: 0 10px;
    height: 1px;
    position: absolute;
    z-index: -1;
    top: 50%;
    transform: translateY(-50%);
    background-color: #ccc;
}
.range-simulate {
    width: calc(100% - 120px);
    margin: 0 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: absolute;
    z-index: -1;
    top: 50%;
    transform: translateY(-50%);
}
.range-simulate li {
    position: relative;
    list-style: none;
    width: 1px;
    height: 10px;
    background-color: #ccc;
}
article {
    text-align: center;
}
</style>

javascript

这里的 js 部分就很简单了,简直是送分题:

var fontModel = ['14px', '16px', '18px', '20px', '22px'];
var article = document.getElementById('article');

document.getElementById('adjust_font').addEventListener('input', function (e) {
    article.setAttribute('style', `font-size: ${fontModel[e.target.value]}`);
}, false);

总结

两种方法实现起来看起来都挺简单,但是综合考虑还是第二种方法优先,我考虑的方面主要有三点:

  • 当各字体的5个标记点不是一条竖线,而是一个圆或者其他形状的时候,我们需要计算圆的中心点,而众所周知移动端我们可能会用 rem 或者其他单位,这时候计算起来比较棘手了;
  • 在 touchmove 事件中处理了很多计算问题,比较消耗资源,有可能会造成用户体验不佳;
  • touchmove 事件的兼容性,Safari 暂不支持,有可能会造成困扰。

第三点在我目前的项目中,在微信浏览器是不需要考虑的。

该文章首发于https://blog.bingqichen.me/

查看原文

赞 0 收藏 3 评论 0

BingqiChen 发布了文章 · 2018-03-30

移动端字体大小调节器实现

最近在一个微信H5项目中需要用到字体大小调节器,就看了一下QQ中的功能,就做了一个相似的,方法也不止一种,而且都不难。

img1

方法1

html

<div class="wrap">
    <div class="line"></div>
    <ul class="font-adjust">
        <li data-size="14px" class="selected"></li>
        <li data-size="16px"></li>
        <li data-size="18px"></li>
        <li data-size="20px"></li>
        <li data-size="22px"></li>
    </ul>
</div>
<article style="font-size: 14px">
    圣诞已过<br>再加上看过各个大牌出的辣眼睛狗年限定以后<br>我以为未来的几个月应该是没有什么东西<br>能骗到我的钱了<br>直到...看到了ysl的春季限定...<br>我知道<br>钱包又要完。蛋 。了。
</article>

style

<style type="text/css">
* {
    margin: 0;
    padding: 0;
    border: none;
    box-sizing: border-box;
}
.wrap {
    position: relative;
    padding: 0 50px;
    border: 1px solid #000;
}
.line {
    width: calc(100% - 100px);
    height: 1px;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    background-color: #ccc;
}
.font-adjust {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 50px;
}
.font-adjust li {
    position: relative;
    list-style: none;
    width: 1px;
    height: 10px;
    background-color: #ccc;
}
.font-adjust li::after {
    content: '';
    display: block;
    position: absolute;
    width: 20px;
    height: 20px;
    top: 50%;
    left: 50%;
    border-radius: 50%;
    transform: translate(-50%, -50%);
    background-color: transparent;
}
.font-adjust li.selected::after {
    background-color: #fff;
    border: 1px solid #eee;
    box-shadow: 0 1px 1px 0 #999;
}
article {
    text-align: center;
}
</style>

我们得到这样的页面:

img2

javascript

最简单的单击目标点可以设置对应字体大小:

$('.font-adjust li').on('click', function(e) {
    $('article').attr('style', `font-size: ${$(this).attr('data-size')}`); // 设置字体大小
    $('.font-adjust li').removeClass('selected');
    $(this).addClass('selected');
})

在QQ的字体设置中,只要点击起始点在调节区域,之后在整个窗口左右滑动也可以达到调节字体的目的,我这里用的是 touchmove 事件,利用它返回的位置信息,判断当前距离哪个点的水平位置最近,就使用哪个字体大小,代码如下:

var fontModel = ['14px', '16px', '18px', '20px', '22px']; // 可供选择的字体序列
var current = 0; // 当前使用的字体在序列中的位置下标
var fontModelsPos = $('.font-adjust li').map(function (index) { // 获得每个标记点的位置 x
    return $('.font-adjust li').eq(index)[0].offsetLeft;
})

$('.font-adjust').on('touchmove', function (e) {
    e.preventDefault();
    var min = {
        i: 0,
        dis: Math.abs(fontModelsPos[0] - e.changedTouches[0].clientX)
    };
    for(var i = 1; i < 5; i++){ // 获得最近标记点
        var dis = Math.abs(fontModelsPos[i] - e.changedTouches[0].clientX); // 计算触控点和各标记点的距离
        if (dis < min.dis) { // 找出最近的那个
            min = { i: i, dis: dis }
        }
    }
    if (min.i != current) { // 字体大小改变
        current = min.i;
        $('.font-adjust li').removeClass('selected');
        $('.font-adjust li').eq(min.i).addClass('selected');
        $('article').attr('style', `font-size: ${fontModel[min.i]}`)
    }
})

效果:

img3

方法2

emmmmm,后来隐约记得有个元素可以充当这个来调节器来用,是它,是它,就是它:inputinput 中有一个属性 type="range" ,使用它可以更方便的完成上述功能。

html

<div class="wrap">
    <input type="range" min="0" max="4" step="1" value="0" id="adjust_font">
    <!-- 以下模拟横线和定位点 -->
    <div class="line"></div>
    <ul class="range-simulate">
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
    </ul>
</div>

这里还是保留了上面的部分代码(5个定位点和1条横线),因为 input 是没有这些点的,横线可以通过更改默认滑轨的样式成为一条细线,但是当滑块在两端时,横线的顶点并不在滑块中央,不介意的话问题也不大。

如下的透视图展示的就是顶点在滑块正中央:

img4

style

<style type="text/css">
* {
    margin: 0;
    padding: 0;
    border: none;
    box-sizing: border-box;
}
.wrap {
    position: relative;
    padding: 10px 50px;
    border: 1px solid #000;
}
input[type=range] {
    -webkit-appearance: none;
    width: 100%;
    background-color: transparent;
}
input[type=range]::-webkit-slider-runnable-track { // 滑轨
    height: 20px;
}
input[type=range]::-webkit-slider-thumb { // 滑块
    -webkit-appearance: none;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background-color: #fff;
    border: 1px solid #eee;
    box-shadow: 0 1px 1px 0 #999;
}
input[type=range]:focus {
    outline: none;
}
.line {
    width: calc(100% - 120px);
    margin: 0 10px;
    height: 1px;
    position: absolute;
    z-index: -1;
    top: 50%;
    transform: translateY(-50%);
    background-color: #ccc;
}
.range-simulate {
    width: calc(100% - 120px);
    margin: 0 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: absolute;
    z-index: -1;
    top: 50%;
    transform: translateY(-50%);
}
.range-simulate li {
    position: relative;
    list-style: none;
    width: 1px;
    height: 10px;
    background-color: #ccc;
}
article {
    text-align: center;
}
</style>

javascript

这里的 js 部分就很简单了,简直是送分题:

var fontModel = ['14px', '16px', '18px', '20px', '22px'];
var article = document.getElementById('article');

document.getElementById('adjust_font').addEventListener('input', function (e) {
    article.setAttribute('style', `font-size: ${fontModel[e.target.value]}`);
}, false);

总结

两种方法实现起来看起来都挺简单,但是综合考虑还是第二种方法优先,我考虑的方面主要有三点:

  • 当各字体的5个标记点不是一条竖线,而是一个圆或者其他形状的时候,我们需要计算圆的中心点,而众所周知移动端我们可能会用 rem 或者其他单位,这时候计算起来比较棘手了;
  • 在 touchmove 事件中处理了很多计算问题,比较消耗资源,有可能会造成用户体验不佳;
  • touchmove 事件的兼容性,Safari 暂不支持,有可能会造成困扰。

第三点在我目前的项目中,在微信浏览器是不需要考虑的。

该文章首发于https://blog.bingqichen.me/

查看原文

赞 0 收藏 3 评论 0

认证与成就

  • 获得 77 次点赞
  • 获得 15 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 11 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-07-23
个人主页被 832 人浏览