wupengyu

wupengyu 查看完整档案

北京编辑河北科技大学  |  计算机科学与技术 编辑京东商城  |  前端开发 编辑 teapot-py.github.io/ 编辑
编辑

写作是为了更好的思考

个人动态

wupengyu 关注了专栏 · 2020-03-18

程序生涯

程序人生,感慨,杂谈! 目前偏前端!

关注 371

wupengyu 赞了文章 · 2020-03-18

Hybrid App技术解析 -- 原理篇

引言

随着 Web 技术和移动设备的快速发展,Hybrid 技术已经成为一种最主流最常见的方案。一套好的 Hybrid架构方案 能让 App 既能拥有极致的体验和性能,同时也能拥有 Web技术 灵活的开发模式、跨平台能力以及热更新机制,想想是不是都鸡冻不已。。😄。本系列文章是公司在这方面实践的一个总结,包含了原理解析、方案选型与实现、实践优化等方面。

大家可以到github上和我进行讨论哈!

第二篇实战篇 也已经完成了哈~~

现有混合方案

Hybrid App,俗称混合应用,即混合了 Native技术 与 Web技术 进行开发的移动应用。现在比较流行的混合方案主要有三种,主要是在UI渲染机制上的不同:

  1. 基于 WebView UI 的基础方案,市面上大部分主流 App 都有采用,例如微信JS-SDK,通过 JSBridge 完成 H5 与 Native 的双向通讯,从而赋予H5一定程度的原生能力。
  2. 基于 Native UI 的方案,例如 React-Native、Weex。在赋予 H5 原生API能力的基础上,进一步通过 JSBridge 将js解析成的虚拟节点树(Virtual DOM)传递到 Native 并使用原生渲染。
  3. 另外还有近期比较流行的小程序方案,也是通过更加定制化的 JSBridge,并使用双 WebView 双线程的模式隔离了JS逻辑与UI渲染,形成了特殊的开发模式,加强了 H5 与 Native 混合程度,提高了页面性能及开发体验。

以上的三种方案,其实同样都是基于 JSBridge 完成的通讯层,第二三种方案,其实可以看做是在方案一的基础上,继续通过不同的新技术进一步提高了应用的混合程度。因此,JSBridge 也是整个混合应用最关键的部分,例如我们在设置微信分享时用到的 JS-SDK,wx对象 便是我们最常见的 JSBridge:

图片描述

方案选型

任何技术方案的选型,其实都应该基于使用场景和现有条件。基于公司现有情况的几点考虑,在方案一上进一步优化,更加适合我们的需求。

  • 需求 Web技术 快速迭代、灵活开发的特点和线上热更新的机制。
  • 产品的核心能力是强大的拍照与底层图片处理能力,因此单纯的 H5技术能做的事非常有限,不能满足需求,通过 Hybrid 技术来强化H5,便是一种必需。
  • 公司业务上,并没有非常复杂的UI渲染需求,而且 App 中的一系列原生 UI组件 已经非常成熟,因此我们并不强需类似 RN 这样的方案。

因此,如何既能利用 H5 强大的开发和迭代能力,又能赋予 H5 强大的底层能力和用户体验,同时能复用现有的成熟 Native组件,便成为了我们最大的需求点 -- 一套完整又强大的 Hybrid技术架构方案。😠

Hybrid技术原理

Hybrid App的本质,其实是在原生的 App 中,使用 WebView 作为容器直接承载 Web页面。因此,最核心的点就是 Native端 与 H5端 之间的双向通讯层,其实这里也可以理解为我们需要一套跨语言通讯方案,来完成 Native(Java/Objective-c/...) 与 JavaScript 的通讯。这个方案就是我们所说的 JSBridge,而实现的关键,便是作为容器的 WebView,一切的原理都是基于 WebView 的机制。

图片描述

(一) JavaScript 通知 Native

基于 WebView 的机制和开放的 API, 实现这个功能有三种常见的方案:

  • API注入,原理其实就是 Native 获取 JavaScript环境上下文,并直接在上面挂载对象或者方法,使 js 可以直接调用,Android 与 IOS 分别拥有对应的挂载方式。
  • WebView 中的 prompt/console/alert 拦截,通常使用 prompt,因为这个方法在前端中使用频率低,比较不会出现冲突;
  • WebView URL Scheme 跳转拦截

第二三种机制的原理是类似的,都是通过对 WebView 信息冒泡传递的拦截,从而达到通讯的,接下来我们主要从 原理-定制协议-拦截协议-参数传递-回调机制 5个方面详细阐述下第三种方案 -- URL拦截方案。

1. 实现原理

在 WebView 中发出的网络请求,客户端都能进行监听和捕获

2. 协议的定制

我们需要制定一套URL Scheme规则,通常我们的请求会带有对应的协议开头,例如常见的 https://xxx.com 或者 file://1.jpg,代表着不同的含义。我们这里可以将协议类型的请求定制为:

xxcommand://xxxx?param1=1&param2=2

这里有几个需要注意点的是:

(1) xxcommand:// 只是一种规则,可以根据业务进行制定,使其具有含义,例如我们定义 xxcommand:// 为公司所有App系通用,为通用工具协议:

xxcommand://getProxy?h=1

而定义 xxapp:// 为每个App单独的业务协议。

xxapp://openCamera?h=2

不同的协议头代表着不同的含义,这样便能清楚知道每个协议的适用范围。

(2) 这里不要使用 location.href 发送,因为其自身机制有个问题是同时并发多次请求会被合并成为一次,导致协议被忽略,而并发协议其实是非常常见的功能。我们会使用创建 iframe 发送请求的方式。

(3) 通常考虑到安全性,需要在客户端中设置域名白名单或者限制,避免公司内部业务协议被第三方直接调用。

3.协议的拦截

客户端可以通过 API 对 WebView 发出的请求进行拦截:

  • IOS上: shouldStartLoadWithRequest
  • Android: shouldOverrideUrlLoading

当解析到请求 URL 头为制定的协议时,便不发起对应的资源请求,而是解析参数,并进行相关功能或者方法的调用,完成协议功能的映射。

4.协议回调

由于协议的本质其实是发送请求,这属于一个异步的过程,因此我们便需要处理对应的回调机制。这里我们采用的方式是JS的事件系统,这里我们会用到 window.addEventListenerwindow.dispatchEvent这两个基础API;

    1. 发送协议时,通过协议的唯一标识注册自定义事件,并将回调绑定到对应的事件上。
    1. 客户端完成对应的功能后,调用 Bridge 的dispatch API,直接携带 data 触发该协议的自定义事件。

图片描述

通过事件的机制,会让开发更符合我们前端的习惯,例如当你需要监听客户端的通知时,同样只需要在通过 addEventListener 进行监听即可。

Tips: 这里有一点需要注意的是,应该避免事件的多次重复绑定,因此当唯一标识重置时,需要removeEventListener对应的事件。

5.参数传递方式

由于 WebView 对 URL 会有长度的限制,因此常规的通过 search参数 进行传递的方式便具有一个问题,既 当需要传递的参数过长时,可能会导致被截断,例如传递base64或者传递大量数据时。

因此我们需要制定新的参数传递规则,我们使用的是函数调用的方式。这里的原理主要是基于:

Native 可以直接调用 JS 方法并直接获取函数的返回值。

我们只需要对每条协议标记一个唯一标识,并把参数存入参数池中,到时客户端再通过该唯一标识从参数池中获取对应的参数即可。

(二) Native 通知 Javascript

由于 Native 可以算作 H5 的宿主,因此拥有更大的权限,上面也提到了 Native 可以通过 WebView API直接执行 Js 代码。这样的权限也就让这个方向的通讯变得十分的便捷。

  • IOS: stringByEvaluatingJavaScriptFromString
// Swift
webview.stringByEvaluatingJavaScriptFromString("alert('NativeCall')")
  • Android: loadUrl (4.4-)
// 调用js中的JSBridge.trigger方法
// 该方法的弊端是无法获取函数返回值;
webView.loadUrl("javascript:JSBridge.trigger('NativeCall')")

Tips: 当系统低于4.4时,evaluateJavascript 是无法使用的,因此单纯的使用 loadUrl 无法获取 JS 返回值,这时我们需要使用前面提到的 prompt 的方法进行兼容,让 H5端 通过 prompt 进行数据的发送,客户端进行拦截并获取数据。

  • Android: evaluateJavascript (4.4+)
// 4.4+后使用该方法便可调用并获取函数返回值;
mWebView.evaluateJavascript("javascript:JSBridge.trigger('NativeCall')",      new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) {
        //此处为 js 返回的结果
    }
});

基于上面的原理,我们已经明白 JSBridge 最基础的原理,并且能实现 Native <=> H5 的双向通讯机制了。

图片描述

(三) JSBridge 的接入

接下来,我们来理下代码上需要的资源。实现这套方案,从上图可以看出,其实可以分为两个部分:

  • JS部分(bridge): 在JS环境中注入 bridge 的实现代码,包含了协议的拼装/发送/参数池/回调池等一些基础功能。
  • Native部分(SDK):在客户端中 bridge 的功能映射代码,实现了URL拦截与解析/环境信息的注入/通用功能映射等功能。

我们这里的做法是,将这两部分一起封装成一个 Native SDK,由客户端统一引入。客户端在初始化一个 WebView 打开页面时,如果页面地址在白名单中,会直接在 HTML 的头部注入对应的 bridge.js。这样的做法有以下的好处:

  • 双方的代码统一维护,避免出现版本分裂的情况。有更新时,只要由客户端更新SDK即可,不会出现版本兼容的问题;
  • App的接入十分方便,只需要按文档接入最新版本的SDK,即可直接运行整套Hybrid方案,便于在多个App中快速的落地;
  • H5端无需关注,这样有利于将 bridge 开放给第三方页面使用。

这里有一点需要注意的是,协议的调用,一定是需要确保执行在bridge.js 成功注入后。由于客户端的注入行为属于一个附加的异步行为,从H5方很难去捕捉准确的完成时机,因此这里需要通过客户端监听页面完成后,基于上面的回调机制通知 H5端,页面中即可通过window.addEventListener('bridgeReady', e => {})进行初始化。

(四) App中 H5 的接入方式

将 H5 接入 App 中通常有两种方式:

(1) 在线H5,这是最常见的一种方式。我们只需要将H5代码部署到服务器上,只要把对应的 URL地址 给到客户端,用 WebView 打开该URL,即可嵌入。该方式的好处在于:

  • 独立性强,有非常独立的开发/调试/更新/上线能力;
  • 资源放在服务器上,完全不会影响客户端的包体积;
  • 接入成本很低,完全的热更新机制。

但相对的,这种方式也有对应的缺点:

  • 完全的网络依赖,在离线的情况下无法打开页面;
  • 首屏加载速度依赖于网络,网络较慢时,首屏加载也较慢;

通常,这种方式更适用在一些比较轻量级的页面上,例如一些帮助页、提示页、使用攻略等页面。这些页面的特点是功能性不强,不太需要复杂的功能协议,且不需要离线使用。在一些第三方页面接入上,也会使用这种方式,例如我们的页面调用微信JS-SDK。

(2) 内置包H5,这是一种本地化的嵌入方式,我们需要将代码进行打包后下发到客户端,并由客户端直接解压到本地储存中。通常我们运用在一些比较大和比较重要的模块上。其优点是:

  • 由于其本地化,首屏加载速度快,用户体验更为接近原生;
  • 可以不依赖网络,离线运行;

但同时,它的劣势也十分明显:

  • 开发流程/更新机制复杂化,需要客户端,甚至服务端的共同协作;
  • 会相应的增加 App 包体积;

这两种接入方式均有自己的优缺点,应该根据不同场景进行选择。

总结

本文主要解析了现在Hybrid App的发展现状和其基础原理,包含了

  • JavaScript 通知 Native
  • Native 通知 Javascript
  • JSBridge 的接入
  • H5 的接入

只有在了解了其最本质的实现原理后,才能对这套方案进行实现以及进一步的优化。接下来,我们将基于上面的理论,继续探讨如何把这套方案的真正代码实现以及方案优化方案,请继续 第二篇实战篇。欢迎大家一起讨论!更多文章内容请到github。感谢!😊

查看原文

赞 180 收藏 143 评论 21

wupengyu 关注了用户 · 2020-01-22

疯狂的技术宅 @evilboy

资深技术宅,爱好广泛,兴趣多变。博览群书,喜欢扯淡。十八种语言样样稀松。想要了解更多,请关注微信公众号:充实的脑洞

关注 5928

wupengyu 发布了文章 · 2020-01-20

react核心

react核心思想

简单来说,就是virtual dom & react diff。
我们都知道在前端开发中,js运行很快,dom操作很慢,而react充分利用了这个前提。在react中render的执行结果是树形结构的javascript对象,当数据(state || props)发生变化时,会生成一个新的树形结构的javascript对象,这两个javascript对象我们可以称之为virtual dom。然后对比两个virtual dom,找出最小的有变化的点,这个对比的过程我们称之为react diff,将这个变化的部分(patch)加入到一个队列中,最终批量更新这些patch到dom中。

react执行render和setState进行渲染时主要有两个阶段

  • 调度阶段(Reconciler):React 会自顶向下通过递归, 用新数据生成一颗新树,遍历虚拟dom,diff新老virtual dom树,搜集具体的UI差异,找到需要更新的元素(Patch),放到更新队列中。
  • 渲染阶段(Renderer):遍历更新队列,通过调用宿主环境的API(比如 DOM、Native、WebGL)实际更新渲染对应元素。

引入虚拟dom的好处是什么?

  • js运行很快,dom操作很慢。配合react diff算法,通过对比virtual Dom,可以快速找出真实dom的最小变化,这样前端其实是不需要去关注那个变化的点,把这个变化交给react来做就好,同时你也不必自己去完成属性操作、事件处理、DOM更新,React会替你完成这一切,这让我们更关注我们的业务逻辑而非DOM操作,基于以上两点可大大提升我们的开发效率。
  • 跨浏览器、跨平台兼容。react基于virtual dom自己实现了一套自己的事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,抹平了各个浏览器的事件兼容性问题。跨平台virtual dom为React带来了跨平台渲染的能力。以React Native为例子。React根据virtual dom画出相应平台的ui层,只不过不同平台画的姿势不同而已。

react对性能的提升

关于提升性能,很多人说virtual dom可以提升性能,这一说法实际上是很片面的。因为我们知道,直接操作dom是非常耗费性能的,但是即使我们用了react,最终依然要去操作真实的dom。而react帮我们做的事情就是尽量用最佳的方式有操作dom。如果是首次渲染,virtual dom不具有任何优势,甚至它要进行更多的计算,消耗更多的内存。
react本身的优势在于react diff算法和批处理策略。react在页面更新之前,提前计算好了如何进行更新和渲染DOM,实际上,这个计算过程我们在直接操作DOM时,也是可以自己判断和实现的,但是一定会耗费非常多的精力和时间,而且往往我们自己做的是不如React好的。所以,在这个过程中React帮助我们"提升了性能"。
所以,我更倾向于说,virtual dom帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比DOM操作更快。

什么是jsx?

我们在实现一个React组件时可以选择两种编码方式,第一种是使用JSX编写,第二种是直接使用React.createElement编写。实际上,上面两种写法是等价的,jsx只是为React.createElemen方法的语法糖,最终所有的jsx都会被babel转换成React.createElement。
但是请注意,babel在编译时会判断jsx中组件的首字母,当首字母为小写时,其被认定为原生dom标签,createElement的第一个变量被编译为字符串。当首字母为大写时,其被认定为自定义组件,createElement的第一个变量被编译为对象。

react的生命周期是怎样的?

在react16中,废弃了三个will属性componentWillMount,componentWillReceiveProps,comonentWillUpdate,但是目前还未删除,react17计划会删除,同时通过UNSAFF_前缀向前兼容。
在 React 中,我们可以将其生命周期分为三个阶段。

挂载阶段

  • constructor()
    组件在挂载前,会调用它的构造函数,在构造函数内部必须执行一次super(props),否则不能在constructor内部使用this,constructor通常用于给this.state初始化内部状态,为事件处理函数绑定this。
  • static getDerivedStateFromProps(newProps,prevState)
    是一个静态方法,父组件传入的newProps和当前组件的prevState进行比较,判断时需要更新state,返回值用作更新state,如果不需要则返回null。在render()方法之前调用,并且在初始挂载和后续更新时调用。
  • render()
    render()是组件中唯一必须实现的方法。需要返回以下类型,React元素、数组、fragments、Portals、字符串或者、值类型、布尔类型或null。同时render函数应该是纯函数。不能够调用setState。
  • componentDidMount()

更新阶段

  • static getDerivedStateFromProps(props,state)
  • shouldComponentUpate()
    当props或者state发生变化时,会在渲染前调用。根据父组件的props和当前的state进行对比,返回true/false。决定是否触发后续的 UNSAFE_componentWillUpdate(),render()和componentDidUpdate()。。
  • render()
  • getSnapshotBeforeUpdate(prevProps,prevSteate)
    在render()之后componentDidUpdate()之前调用。此方法的返回值(snaphot)可作为componentDidUpdate()的第三个参数使用。如不需要返回值则直接返回null。
  • componentDidUpdate(prevProps, prevState, snapshot)
    该方法会在更新完成后立即调用。首次渲染不会执行此方法,当组件更新后,可以在此处对dom进行操作。可以在此阶段使用setState,触发render()但必须包裹在一个条件语句里,以避免死循环。

卸载阶段

  • componentWillUnmount()
    会在组件卸载和销毁之前直接调用。此方法主要用来执行一些清理工作,例如:定时器,清除事件绑定,取消网络请求。此阶段不能调用setState,因为组件永远不会重新渲染。

react diff解决什么问题?是怎样的实现思路?

react diff会帮助我们计算出virtual dom中真正变化的部分,并只针对该部分进行实际dom操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染。传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3)。react diff基于一下三个策略实现了O(n)的算法复杂度。

  • Web UI中dom节点跨层级的移动操作特别少,可以忽略不计。
  • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  • 对于同一层级的一组子节点,它们可以通过唯一id进行区分。

基于以上三个前提策略,React分别对tree diff、component diff以及element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。

react中key的作用,能不能用index作为Key。

首先说一下element diff的过程。比如有老的集合(A,B,C,D)和新的集合(B,A,D,C),我们考虑在不增加空间复杂度的情况下如何以O(n)的时间复杂度找出老集合中需要移动的元素。
在react里的思路是这样的,遍历新集合,初始化lastIndex=0(代表访问过的老集合中最右侧的位置),表达式为max(prev.mountIndex, lastIndex),如果当前节点在老集合中的位置即(prev.mountIndex)比lastIndex大说明当前访问节点在老集合中就比上一个节点位置靠后则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。
部分源码为

var lastIndex = 0;
var nextIndex = 0;
for (name in nextChildren) {
    var prevChild = prevChildren && prevChildren[name]; // 老节点
    var nextChild = nextChildren[name]; // 新节点
    if (prevChild === nextChild) { // 如果新节点存在老节点集合里
      // 移动节点
      this.moveChild(prevChild, nextIndex, lastIndex);
      lastIndex = Math.max(prevChild._mountIndex, lastIndex);
      prevChild._mountIndex = nextIndex;
    } else {
      if (prevChild) { // 如果不存在在
        lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        // 删除节点
        this._unmountChild(prevChild);
      }
      // 初始化并创建节点
      this._mountChildAtIndex(
        nextChild, nextIndex, transaction, context
      );
    }
    nextIndex++;
}

// 移动节点
moveChild: function(child, toIndex, lastIndex) {
  if (child._mountIndex < lastIndex) {
    this.prepareToManageChildren();
    enqueueMove(this, child._mountIndex, toIndex);
  }
}

React 16有哪些新特性?

  • render支持返回数组和字符串
  • Error Boundaries
  • createPortal
  • rollup减小文件体积
  • fiber
  • Fragment
  • createRef
  • Strict Mode

React Fiber是什么?解决什么问题?

React Fiber是React对核心算法的一次重新实现。
在协调阶段阶段,以前由于是采用的递归的遍历方式,这种也被称为Stack Reconciler,主要是为了区别Fiber Reconciler取的一个名字。这种方式有一个特点: 一旦任务开始进行,就无法中断,那么js将一直占用主线程,一直要等到整棵virtual dom树计算完成之后,才能把执行权交给渲染引擎,那么这就会导致一些用户交互、动画等任务无法立即得到处理,就会有卡顿,非常的影响用户体验。
页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到60时,页面是流畅的,小于这个值时,用户会感觉到卡顿。1秒60帧,所以每一帧分到的时间是1000/60 ≈ 16ms。所以我们书写代码时力求不让一帧的工作量超过 16ms。如果任意一个步骤所占用的时间过长,超过16ms了之后,用户就能看到卡顿。

Fiber如何实现

简单来说就是时间分片 + 链表结构。而fiber就是维护每一个分片的数据结构。
Fiber利用分片的思想,把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,在每个小片执行完之后,就把控制权交还给React负责任务协调的模块,如果有紧急任务就去优先处理,如果没有就继续更新,这样就给其他任务一个执行的机会,唯一的线程就不会一直被独占。
因此,在组件更新时有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。所以 React Fiber把一个更新过程分为两个阶段:

  • 第一个阶段 Reconciliation Phase,Fiber会找出需要更新的DOM,这个阶段是可以被打断的。
  • 第二个阶段 Commit Phase,是无法别打断,完成dom的更新并展示。

什么是高阶组件

高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧。HOC自身不是React API的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。
我理解的高阶组件是,将组件以参数的方式传递给另外一个函数,在该函数中,对组件进行包装,封装了一些公用的组件逻辑,实现组件的逻辑复用,该函数被称为高阶组件。但是请注意,高阶组件不应修改传入的组件行为。
属性代理

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        user: currentLoggedInUser
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

反向继承

function hoc(ComponentClass) {
    return class HOC extends ComponentClass {
        render() {
            if (this.state.success) {
                return super.render()
            }
            return <div>Loading...</div>
        }
    }
}

export default class ComponentClass extends Component {
    state = {
        success: false,
        data: null
    };
    async componentDidMount() {
        const result = await fetch(...请求);          
     this.setState({
            success: true,
            data: result.data
        });
    }
    render() {
        return <div>主要内容</div>
    }
}

什么是渲染属性

术语 “render prop” 是指一种技术,用于使用一个值为函数的 prop 在 React 组件之间的代码共享。
带有渲染属性(Render Props)的组件需要一个返回 React 元素并调用它的函数,而不是实现自己的渲染逻辑。
我理解的渲染属性是,提供渲染页面的props给子组件,共享可以共享子组件的状态,复用子组件的状态,并告诉子组件如何进行渲染。

import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
// 与 HOC 不同,我们可以使用具有 render prop 的普通组件来共享代码
class Mouse extends React.Component {
  static propTypes = {
    render: PropTypes.func.isRequired
  }
  state = { x: 0, y: 0 }
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  }
  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    )
  }
}
const App = React.createClass({
  render() {
    return (
      <div style={{ height: '100%' }}>
        <Mouse render={({ x, y }) => (
          // render prop 给了我们所需要的 state 来渲染我们想要的
          <h1>The mouse position is ({x}, {y})</h1>
        )}/>
      </div>
    )
  }
})
ReactDOM.render(<App/>, document.getElementById('app'))

什么是React Hooks,它是为了解决什么问题?说一下它的实现原理!

React Hooks 是 React 16.7.0-alpha 版本推出的新特性,它可以让你在不编写class的情况下使用state以及其他的 React特性。React Hooks要解决的问题是状态共享,是继render-props和hoc之后的第三种状态共享方案,不会产生JSX嵌套地狱问题。这个状态指的是状态逻辑,所以称为状态逻辑复用会更恰当,因为只共享数据处理逻辑,不会共享数据本身。

简单实现

let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标

function useState(initialValue) {
  memoizedState[cursor] = memoizedState[cursor] || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memoizedState[currentCursor] = newState;
    render();
  }
  return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray;
  const deps = memoizedState[cursor];
  const hasChangedDeps = deps
    ? !depArray.every((el, i) => el === deps[i])
    : true;
  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedState[cursor] = depArray;
  }
  cursor++;
}

React为什么要在构造函数中调用super(props),为什么要bind(this)?

super代表父类的构造函数,javascript规定如果子类不调用super是不允许在子类中使用this的,这不是React的限制,而是javaScript的限制,同时你也必须给super传入props,否则React.Component就没法初始化this.props
在 React 的类组件中,当我们把事件处理函数引用作为回调传递过去,事件处理程序方法会丢失其隐式绑定的上下文。当事件被触发并且处理程序被调用时,this的值会回退到默认绑定,即值为 undefined,这是因为类声明和原型方法是以严格模式运行。

说一下react事件机制?

react为什么要用自己的事件机制

  • 减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在document上注册一次。
  • 统一规范,解决 ie 事件兼容问题,简化事件逻辑。
  • 对开发者友好。

react的合成事件

SyntheticEvent是react合成事件的基类,定义了合成事件的基础公共属性和方法。react会根据当前的事件类型来使用不同的合成事件对象,比如鼠标单机事件 - SyntheticMouseEvent,焦点事件-SyntheticFocusEvent等,但是都是继承自SyntheticEvent。在合成事件中主要做了以下三件事情。

  • 对原生事件的封装
  • 对某些原生事件的升级和改造
  • 不同浏览器事件兼容的处理

事件注册

组件挂载阶段,根据组件内的声明的事件类型-onclick,onchange等,给document上添加事件addEventListener,并指定统一的事件处理程序dispatchEvent。
通过virtual dom的props属性拿到要注册的事件名,回调函数,通过listenTo方法使用原生的addEventListener进行事件绑定。

事件存储

事件存储,就是把react组件内的所有事件统一的存放到一个二级map对象里,缓存起来,为了在触发事件的时候可以查找到对应的方法去执行。先查找事件名,然后找对对应的组件id相对应的事件。如下图:
8081b073fb2c06f047538b75cc97fc6f.png

setState是异步的?为什么要这么做?setState执行机制?

由执行机制看,setState本身并不是异步的,而是在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后再执行,这个过程给人一种异步的假象。

ReactComponent.prototype.setState = function(partialState, callback) {
  //  将setState事务放进队列中
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
enqueueSetState: function (publicInstance, partialState) {
     // 获取当前组件的instance
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

     // 将要更新的state放入一个数组里
     var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

     //  将要更新的component instance也放在一个队列里
    enqueueUpdate(internalInstance);
}
function enqueueUpdate(component) {
  // 如果没有处于批量创建/更新组件的阶段,则处理update state事务
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 如果正处于批量创建/更新组件的过程,将当前的组件放在dirtyComponents数组中
  dirtyComponents.push(component);
}

这里的partialState可以传object,也可以传function,它会产生新的state以一种Object.assgine()的方式跟旧的state进行合并。
由这段代码可以看到,当前如果正处于创建/更新组件的过程,就不会立刻去更新组件,而是先把当前的组件放在dirtyComponent里,所以不是每一次的setState都会更新组件。这段代码就解释了我们常听说的:setState是一个异步的过程,它会集齐一批需要更新的组件然后一起更新。而batchingStrategy 又是个什么东西呢?
ReactDefaultBatchingStrategy.js

var ReactDefaultBatchingStrategy = {
  // 用于标记当前是否出于批量更新
  isBatchingUpdates: false,
  // 当调用这个方法时,正式开始批量更新
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // 如果当前事务正在更新过程在中,则调用callback,既enqueueUpdate
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
    // 否则执行更新事务
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  }
};

react-router原理

前端路由的原理思路大致上都是相同的,即实现在无刷新页面的条件下切换显示不同的页面。而前端路由的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。目前实现前端路由有两种方式:

通过Hash实现前端路由

路径中hash值改变,并不会引起页面刷新,同时我们可以通过hashchange事件,监听hash的变化,从而实现我们根据不同的hash值展示和隐藏不同UI显示的功能,进而实现前端路由。

通过H5的history实现前端路由

HTML5的History接口,History对象是一个底层接口,不继承于任何的接口。History接口允许我们操作浏览器会话历史记录。
而history的pushState和repalce方法可以实现改变当前页面显示的url,但都不会刷新页面。

未完待续~
参考文档:

react生命周期详解
React diff
react 16新特性
react fiber1
react fiber2
react hooks
react 事件机制
setState机制1
setState机制2
react-router原理
集合

查看原文

赞 0 收藏 0 评论 0

wupengyu 关注了专栏 · 2020-01-14

有赞美业前端团队

关注 2792

wupengyu 关注了专栏 · 2020-01-14

code秘密花园

基础知识、算法、原理、项目、面试。公众号code秘密花园

关注 5211

wupengyu 关注了用户 · 2020-01-14

ConardLi @conardli

Reading makes a full man, conference a ready man, and writing an exact man.

关注 2324

wupengyu 发布了文章 · 2019-12-30

React Hooks实践

9月份开始,使用了React16.8的新特性React Hooks对项目进行了重构,果然,感觉没有被辜负,就像阮一峰老师所说的一样,这个 API 是 React 的未来。

Hooks

React Hooks是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

说是新的特性,但是与其他的版本的迭代不同,它不只是加一点api减一点api的改变。而是完整的一套解决方案。

一个简单的Hook

import React, { useState } from 'react';

function Example() {
  // 声明一个新的叫做 “count” 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

这是React官网关于react使用的第一个例子,使用Hooks的方式实现了一个计数器功能。
useState是什么?在这里我们认为useState就是一个Hook。
下面介绍 React 2个最常用的钩子,useState,useEffect。

useState

这个Hook,应该是我们使用最多的一个Hook了。通过在函数组件里调用它来给组件添加一些内部 state。
useState方法会返回一个包含2个值的数组,第一个值是当前状态。第二个值更新这个状态的函数,它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。我们一般会通过解构的方式获取其值。

使用

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

这里我们将内部状态count初始化为0,在count发生变化时,会引起组件的会重新渲染。同时,我们通过解构的方式,给该状态进行了赋值。

更新

<button onClick={() => setCount(count + 1)}>
        Click me
      </button>

直接调用useState返回值的第二个参数,即可完成更新。

useEffect

我在写React ,类组件的时候,最不愿意写的就是生命周期方法,一方面是因为生命周期方法比较多,另一方面其实也比较容易出bug。比如说,我们想要实现一个监听props的某个值的变化,进而进行一些特殊操作,就可能需要两种生命周期的方法配合。但是在Hooks里,我们只需要用到useEffect就可以实现了。

使用

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 类似于componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 更新文档的标题
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect方法接受2个参数,第一个参数是一个函数,是在第一次渲染以及之后更新渲染之后会进行的副作用,强调一点,该函数可以有返回值,但是该返回值必须是一个函数,会在组件被销毁时执行。
第二个参数是可选的,是一个数组,数组中存放的是需要监听的属性。即当数组中的属性发生变化时,第一个参数的函数会被调用,如果是空数组,则在第一次渲染时会被调用。
基于以上两个最主要的Hook,我们基本上可以满足于我们大部分的需求。但是如果想要对组件进行优化,则需要另外两个Hook。useCallback和useMemo。

useCallback

useCallback接收一个内联回调函数参数和一个依赖项数组(子组件依赖父组件的状态,即子组件会使用到父组件的值) ,useCallback 会返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

useMemo

useMemo把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

基于以上的Hook我们基本上可以完成大部分的功能了,除了上文重点介绍的useState和useEffect,react还给我们提供来很多有用的hooks useContext useReducer useRef useImperativeMethods useMutationEffect useLayoutEffect,这里就不一一介绍了,如果想要使用Hooks,这些api也是需要了解的,具体使用可以参考下面的文档。

参考文档

React 官网

查看原文

赞 6 收藏 5 评论 0

wupengyu 赞了文章 · 2019-11-19

“寒冬”三年经验前端面试总结(含头条、百度、饿了么、滴滴等)之CSS篇

前言

不论是寒冬还是暖冬,找工作之前都需要做好充足的准备,面试的时候才能做到游刃有余。此文是把我最近找工作准备的以及笔试面试中涉及到的手写题做一个总结。给自己,也给需要的同学。

CSS是前端必须要掌握的技能之一。一般面试也都会从CSS开始。所以CSS问题答的好坏会直接影响你在面试官心中的形象。

本文主要介绍面试中常会遇到的CSS问题及给出建议性的答案。


往期

  1. “寒冬”三年经验前端面试总结(含头条、百度、饿了么、滴滴等)
  2. "寒冬"三年经验前端面试总结(含头条、百度、饿了么、滴滴等)之手写题(一)
  3. "寒冬"三年经验前端面试总结(含头条、百度、饿了么、滴滴等)之手写题(二)
  4. "寒冬"三年经验前端面试总结(含头条、百度、饿了么、滴滴等)之手写题(promise篇)

盒模型

盒模型感觉是刚学前端的时候就会接触到的问题。元素都是按照盒模型的规则布局在页面中的。盒模型由 margin + border + padding + content 四个属性组成,分为两种:W3C的标准盒模型和IE盒模型。

W3C的标准盒模型

width = content,不包含 border + padding

IE盒模型

width = border + padding + content

相互转换

二者之间可以通过CSS3的 box-sizing 属性来转换。

box-sizing: content-box 是W3C盒模型

box-sizing: border-box 是IE盒模型

垂直居中的方法

垂直居中的方法,如果全写出来,有10多种。面试的时候一般都会说比较常用的几种。flexposition + transformposition + 负margin是最常见的三种情况。

<div class="outer">
    <div class="inner"></div>
</div>

方法一:flex

.outer{
    display: flex;
    justify-content: center;
    align-items: center
}

方法二: position + transform, inner宽高未知

.outer{
    position:relative;
}
.inner{
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
}

方法三:position + 负margin, inner宽高已知

.outer{
    position: relative;
}
.inner{
    width: 100px;
    height: 100px;
    position: absolute;
    left: 50%;
    top: 50%;
    margin-left: -50px;
    margin-top: -50px;
}

方法四:设置各个方向的距离都是0,再将margin设为auto,也可以实现,前提是inner宽高已知

.outer {
    position: relative;
}
.inner {
    width: 100px;
    height: 100px;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    margin: auto;
}

三栏布局

三栏布局是很常见的一种页面布局方式。左右固定,中间自适应。实现方式有很多种方法。

第一种:flex

<div class="container">
    <div class="left">left</div>
    <div class="main">main</div>
    <div class="right">right</div>
</div>
.container{
    display: flex;
}
.left{
    flex-basis:200px;
    background: green;
}
.main{
    flex: 1;
    background: red;
}
.right{
    flex-basis:200px;
    background: green;
}

第二种:position + margin

<div class="container">
    <div class="left">left</div>
    <div class="right">right</div>
    <div class="main">main</div>
</div>
body,html{
    padding: 0;
    margin: 0;
}
.left,.right{
    position: absolute;
    top: 0;
    background: red;
}
.left{
    left: 0;
    width: 200px;
}
.right{
    right: 0;
    width: 200px;
}
.main{
    margin: 0 200px ;
    background: green;
}

第三种:float + margin

<div class="container">
    <div class="left">left</div>
    <div class="right">right</div>
    <div class="main">main</div>
</div>
body,html{
    padding:0;
    margin: 0;
}
.left{
    float:left;
    width:200px;
    background:red;
}
.main{
    margin:0 200px;
    background: green;
}
.right{
    float:right;
    width:200px;
    background:red;
}

CSS权重计算方式

CSS基本选择器包含ID选择器、类选择器、标签选择器、通配符选择器。
正常情况下,一般都能答出!important > 行内样式 > ID选择器 > 类选择器 > 标签选择器 > 通配符选择器

但如果这几种选择器同时作用于一个元素时,该元素最后应用哪个样式呢?这就涉及到权重计算的问题。
关于权重计算,有两种不同的计算方式:一种是以二进制的规则计算,一种是以1,10,100,1000这种的计算方式。我更倾向于二进制的这种方式。

各选择器权值:

  • 内联样式,权值为1000
  • ID选择器,权值为0100
  • 类,伪类和属性选择器,权值为0010
  • 标签选择器和伪元素选择器,权值为0001
  • 通配符、子选择器、相邻选择器等,权值为0000
  • 继承的样式没有权值

比较方式:

如果层级相同,继续往后比较,如果层级不同,层级高的权重大,不论低层级有多少个选择器。

BFC

BFC的全称为 Block Formatting Context,也就是块级格式化上下文的意思。

以下方式都会创建BFC:

  • 根元素(html)
  • 浮动元素(元素的 float 不是 none)
  • 绝对定位元素(元素的 position 为 absolute 或 fixed)
  • 行内块元素(元素的 display 为 inline-block)
  • 表格单元格(元素的 display为 table-cell,HTML表格单元格默认为该值)
  • 表格标题(元素的 display 为 table-caption,HTML表格标题默认为该值)
  • 匿名表格单元格元素(元素的 display为 table、table-row、table-row-group、table-header-group、table-footer-group(分别是HTML table、row、tbody、thead、tfoot的默认属性)或 inline-table)
  • overflow 值不为 visible 的块元素
  • display 值为 flow-root 的元素
  • contain 值为 layout、content或 paint 的元素
  • 弹性元素(display为 flex 或 inline-flex元素的直接子元素)
  • 网格元素(display为 grid 或 inline-grid 元素的直接子元素)
  • 多列容器(元素的 column-count 或 column-width 不为 auto,包括 column-count 为 1)

column-span 为 all 的元素始终会创建一个新的BFC,即使该元素没有包裹在一个多列容器中(标准变更,Chrome bug)。

BFC布局规则:

  1. 内部的box会在垂直方向,一个接一个的放置。
  2. box垂直方向的距离有margin决定。属于同一个BFC的两个相邻box的margin会发生重叠。3. 每个元素的左外边距与包含块的左边界相接触,即使浮动元素也是如此。
  3. BFC的区域不会与float的元素区域重叠。
  4. 计算BFC的高度时,浮动子元素也参与计算。
  5. BFC就是页面上一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然。

BFC能解决的问题:

  1. 父元素塌陷
  2. 外边距重叠
  3. 清除浮动

清除浮动的方法

清除浮动主要是为了防止父元素塌陷。清除浮动的方法有很多,常用的是 clearfix 伪类。

方法一:clearfix

<div class="outer clearfix">
    <div class="inner">inner</div>
</div>
.outer{
    background: blue;
}
.inner{
    width: 100px;
    height: 100px;
    background: red;
    float: left;
}
.clearfix:after{
    content: "";
    display: block;
    height: 0;
    clear:both;
    visibility: hidden;
}

方法二:额外加一个div,clear:both

<div class="container">
    <div class="inner"></div>
    <div class="clear"></div>
</div>
.container{
    background: blue;
}
.inner {
    width: 100px;
    height: 100px;
    background: red;
    float: left;
}
.clear{
    clear:both;
}

方法三:触发父盒子BFC,overflow:hidden

<div class="outer">
    <div class="inner">inner</div>
</div>
.outer{
    background: blue;
    overflow: hidden;
}
.inner {
    width: 100px;
    height: 100px;
    background: red;
    float: left;
}

flex布局

flex 布局现在已经很普及的在用了。垂直居中用 flex 实现起来很简单。关于 flex 的属性也不是很多,父容器和子容器各6个,一共12个,比较好记。

下面是我复习flex属性时的一张导图。

position属性

position属性的重要性应该没啥可说的了。想必谁都回答的上来。

  • absolute 绝对定位,相对于 static 定位以外的第一个父元素进行定位。
  • relative 相对定位,相对于其自身正常位置进行定位。
  • fixed 固定定位,相对于浏览器窗口进行定位。
  • static 默认值。没有定位,元素出现在正常的流中。
  • inherit 规定应该从父元素继承 position 属性的值。

但是要注意一个问题,absolute 是相对于父元素的哪个属性进行定位的?通过下面的例子我们来看一看。

.container{
    position: relative;
    width: 30px;
    height: 30px;
    margin: 20px;
    border: 10px solid red;
    padding: 10px;
    background: blue;
}
.inner {
    position: absolute;
    width: 10px;
    height: 10px;
    top: 0;
    left: 0;
    background: pink;
}

从上图可以看出,是相对于 static 定位以外的第一个父元素的 padding 来定位的。

CSS3中新增了一个 position:sticky 属性,该属性的作用类似 position:relativeposition:fixed的结合。元素在跨越特定阈值前为相对定位,之后为固定定位。必须指定 top, right, bottomleft 四个阈值其中之一,才可使粘性定位生效。否则其行为与相对定位相同。但 sticky 尚在实验性阶段。

如何实现一个自适应的正方形

方法1:利用CSS3的vw单位

vw 会把视口的宽度平均分为100份

.square {
    width: 10vw;
    height: 10vw;
    background: red;
}

方法2:利用margin或者padding的百分比计算是参照父元素的width属性

.square {
    width: 10%;
    padding-bottom: 10%; 
    height: 0; // 防止内容撑开多余的高度
    background: red;
}

如何用css实现一个三角形

方法1: 利用border属性

利用盒模型的 border 属性上下左右边框交界处会呈现出平滑的斜线这个特点,通过设置不同的上下左右边框宽度或者颜色即可得到三角形或者梯形。

.triangle {
    height:0;
    width:0;
    border-color:red blue green pink;
    border-style:solid;
    border-width:30px;
}

如果想实现其中的任一个三角形,把其他方向上的 border-color 都设置成透明即可。

.triangle {
    height:0;
    width:0;
    border-color:red transparent transparent transparent;
    border-style:solid;
    border-width:30px;
}

方法二: 利用CSS3的clip-path属性

不了解 clip-path 属性的可以先看看 MDN 上的介绍:chip-path

.triangle {
    width: 30px;
    height: 30px;
    background: red;
    clip-path: polygon(0px 0px, 0px 30px, 30px 0px); // 将坐标(0,0),(0,30),(30,0)连成一个三角形
    transform: rotate(225deg); // 旋转225,变成下三角
}

写在最后

有错误之处还请小伙伴们及时指出,以免误人子弟。想看往期内容,翻到页面最上面有链接~

查看原文

赞 145 收藏 111 评论 3

wupengyu 赞了文章 · 2019-11-11

canvas实现平铺水印

欲实现的水印平铺的效果图如下:

canvas实现平铺水印效果图

从图上看,应该做到以下几点:

  1. 文字在X和Y方向上进行平铺;

  2. 文字进行了一定的角度的旋转;

  3. 水印作为背景,其z-index位置应位于页面内容底部, 即不能覆盖页面主内容;

  4. 平铺的水印应能随窗口大小改变进行自适应。

思路:

首先我们先在canvas上绘制如下图所示一小块画布:

小画布绘制单一水印

var tpl = '<canvas id = "watermark" width = "160px"  height = "100px" style="display:none;"></canvas>';

将单一水印绘制在canvas画布里,然后将canvas节点插入到需要平铺水印的容器里,例如这里将canvas标签插入到body里面。

$(document.body).append(tpl);

这里简要说明:canvas标签width height属性,若不进行指定,则会有个默认的大小(300px * 150px),并且,这块画布的大小使用外部css文件设定宽高是无效的。

下面开始在canvas里面绘制单一水印:

var cw = $('#watermark')[0];   
var ctx = cw.getContext("2d");   //返回一个用于在画布上绘图的环境
ctx.cearRect(0,0,160,100);  //绘制之前画布清除
ctx.font="20px 黑体";  
ctx.rotate(-20*Math.PI/180);
ctx.fillStyle = "rgba(100,100,100,0.1)";
ctx.fillText("465dd92381", -20, 80);
ctx.rotate('20*Math.PI/180');  //坐标系还原

实现了一小块画布的绘制以后,再建一个canvas画布(id为repeat-watermark):

var tplr = '<canvas id = "repeat-watermark"></canvas>'; 
$(document.body).append(tplr);

为整块画布设定样式:

#repeat-watermark{
   position:fixed;
   z-index:-1;
   top:0;
   background: #fff;
}

z-index的值可以根据需要调整,使其位于页面底部平铺。
另作一点说明:位于水印上层的页面如果想让水印始终可见,可以将页面中使用的颜色使用rgba设置。

下面将前面绘制的id为watermark的canvas 在当前的canvas(id为repeat-watermark)里采用createPattern方法进行平铺:

var crw = $('#repeat-watermark')[0];  
crw.width = $(document).width();
crw.height = $(document).height();
ctxr = crw.getContent("2d");
ctxr.clearRect(0,0,crw.width,crw.height);  //清除整个画布 
var pat = ctxr.createPattern(cw, "repeat");    //在指定的方向上重复指定的元素  
ctxr.fillStyle = pat;  
ctxr.fillRect(0, 0, crw.width, crw.height);

此时还有一个问题,由于水印绘制只随着页面进行了一次加载,因而当窗口改变大小时,页面背景水印不会跟随改变进行填充或者裁剪,而是会出现空白,因此,将上述绘制水印封装为draw方法,然后添加以下事件:

$(window).resize(function(){
   var w = $(document).width();
   var h = $(document).height();
   self.draw(w, h);
});

下面附上源码:

'use strict';

require('./watermark.css');

var Watermark = function(container, options) {
    var self = this;
    self.opt = {
        docWidth: $(document).width(),
        docHeight: $(document).height(),
        fontStyle: "20px 黑体", //水印字体设置
        rotateAngle: -20 * Math.PI / 180, //水印字体倾斜角度设置
        fontColor: "rgba(100, 100, 100, 0.1)", //水印字体颜色设置
        firstLinePositionX: -20, //canvas第一行文字起始X坐标
        firstLinePositionY: 80, //Y
        SecondLinePositionX: 0, //canvas第二行文字起始X坐标
        SecondLinePositionY: 70 //Y
    };
    $.extend(self.opt, options);
    self.render(container);
    self.draw(self.opt.docWidth, self.opt.docHeight);
    self.events();
};

Watermark.prototype = {
    render: function(d) {
        var self = this;
        d.append(tpl);
    },

    draw: function(docWidth, docHeight) {
        var self = this;
        var cw = $('#watermark')[0];
        var crw = $('#repeat-watermark')[0];

        crw.width = docWidth;
        crw.height = docHeight;

        var ctx = cw.getContext("2d");
        //清除小画布
        ctx.clearRect(0, 0, 160, 100);
        ctx.font = self.opt.fontStyle;
        //文字倾斜角度
        ctx.rotate(self.opt.rotateAngle);

        ctx.fillStyle = self.opt.fontColor;
        //第一行文字
        ctx.fillText(self.opt.watermark, self.opt.firstLinePositionX, self.opt.firstLinePositionY);
        //第二行文字 
        //ctx.fillText(window.watermark.mobile, self.opt.SecondLinePositionX, self.opt.SecondLinePositionY);
        //坐标系还原
        ctx.rotate(-self.opt.rotateAngle);

        var ctxr = crw.getContext("2d");
        //清除整个画布
        ctxr.clearRect(0, 0, crw.width, crw.height);
        //平铺--重复小块的canvas
        var pat = ctxr.createPattern(cw, "repeat");
        ctxr.fillStyle = pat;

        ctxr.fillRect(0, 0, crw.width, crw.height);
    },
    events: function() {
        var self = this;
        $(window).resize(function() {
            var w = $(document).width();
            var h = $(document).height();
            self.draw(w, h);
        });
    }

};

var tpl = '<canvas id = "watermark" width = "160px"  height = "100px" style="display:none;"></canvas>' + '<canvas id = "repeat-watermark"></canvas>';

module.exports = Watermark;
查看原文

赞 9 收藏 13 评论 1

认证与成就

  • 获得 500 次点赞
  • 获得 55 枚徽章 获得 1 枚金徽章, 获得 15 枚银徽章, 获得 39 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-08-22
个人主页被 2.5k 人浏览