React Router 相关配置

是基于 HTML5 history API 和 popstate 事件所封装的一个高阶组件,在不考虑过低版本(如 IE9 及以下,安卓4.1及以下等)兼容的情况下,可以说是本文优先推荐的组件。其基本属性如下:

basename: String

配置整个 webapp 的基准 URL,比如你的资源放在了服务器 /web 下面,那么可以统一配置 basename=’/web’,此时访问home页面,实际 URL 为 ‘/web/home’

getUserConfirmation: func

导航前所要用的函数,默认是 window.confirm,目前很少用到此功能。

forceRefresh: bool

这个属性为true的时候,页面跳转时会强制刷新。一般在不支持HTML5 history API的时候可能会用到这个属性。

keyLength: number

location.key的长度,默认是6。location.key是随着页面不断变化的,详情可以见下文对location对象的介绍。

children: node

渲染子元素。这个实际就是 react 组件的一个特性了,而不是 BrowserRouter 这个组件独有的特性。
下面是关于 组件的示例代码,可以复制到前文 React Router 简介 中的代码事例中尝试一下,也可以参考完整版 demo2

// 导航前的函数
const getConfirmation = (message, callback) => {
const allowTransition = window.confirm(message)
  callback(allowTransition)
}
// 判断是否支持 HTML5 history API
const supportsHistory = !('pushState' in window.history)
class App extends Component {
  render() {
    return (
      <div className="App">
        <BrowserRouter
            basename='/web'
            getUserConfirmation={getConfirmation('hello world', () => {console.log('success')})}
            forceRefresh={supportsHistory} // false: 不刷新 true: 刷新
            keyLength={12} // location.key长度为12
        >
          <Switch> 
            <Route component={Nav} path='/' exact /> // 实际路径 /web
            <Route component={Home} path='/home' /> // 实际路径 /web/home
            <Route component={Content} path='/content' /> // 实际路径 /web/page
            <Route component={Log} path='/log' />  // 实际路径 /web/log
          </Switch>
        </BrowserRouter>
      </div>
    );
  }
}

是基于 window.location.hash 和 hashchange 事件所封装的路由组件。其特点是兼容性较好。但是不支持 location.key 和location.state,也不支持 forceRefresh,keyLength。其 basenamegetUserConfirmationchildren 的使用方法与 一致,接下来描述其独有属性:

hashType: string

用于window.location.hash 的编码类型。
– “slash” – 创建的url为 #/ 或者 #/web/home
– “noslash” – 创建的url为 # 或者 #web/home
– “hashbang” – 创建的url为 #!/ 或者 #!/web/home
用法类似,故不再做 demo。

是一种声明式的导航组件。用法比较简单。

to: string

参数为字符串时,直接跳转到指定路径。

to:object

参数为对象时,可以传递以下字段:
pathname: 页面路径
– search: 页面 url 参数(window.location.search)
– hash: 页面 hash(window.location.hash)
– state: 隐性传递的一些参数
传递的字段可以在相应页面组件的props.location中取得。如 demo3 中 /src/containers/Nav/Nav.js进入内容页的导航:

// demo3中 /src/containers/Nav/Nav.js 进入内容页的导航
<Link to={
  {
    path: '/content',
    search: 'hello',
    hash: 'world',
    state: {
      name: '测试员1'
    }
  }
}>

而对应的 /src/containers/Content/Content.js 内容页对其渲染:

render() {
  return (
    <div>
      <Nav />
      <div className='content'>我是{this.props.location.state.name},我的search为:{this.props.location.search},我的hash为:{this.props.location.hash}</div>
    </div>
  )
}

最终渲染结果为:我是测试员1,我的search为:?hello,我的hash为:#world

replace: bool

如果跳转的下一个页面 replace 为 true,当页面回退的时候,会回退到上一个页面,而不是当前页。比如你从 ‘/’ 跳转至 ‘/content’ 页面,又从 ‘/content’ 页面跳转至 ‘/log’, 导航至 ‘/log’ 页面的Link为:

<Link to='/log' replace />

此时点击浏览器回退,会直接回退至’/’页面。

innerRef: function

此属性可以获取真实的 DOM 节点。如:

const refCallback = node => {
  // node在未渲染的时候是null,在渲染的时候是真实dom
  console.log(node)
}
...
<Link to='/other' innerRef={refCallback}>

组件的具体示例,可以查看 demo3

组件可以说是一个非常重要的组件了,它的主要功能是当访问的地址与 Route 上的路径匹配时,渲染出对应的 UI 页面。因此,你应该熟练地去学习并理解它。
例如以下示例代码:

import { BrowserRouter as Router, Route } from 'react-router-dom'
<Router>
  <div>
    <Route exact path="/" component={Home}/>
    <Route path="/content" component={Content}/>
  </div>
</Router>

当你访问 URL: / 的时候,就显示home页面,类似于:

<div>
  <Home />
</div>

而当你访问 URL: /content的时候,则渲染出 content 页面,类似于:

<div>
  <Content />
</div>

三种渲染方法

组件提供了三种渲染方法供你使用,分别是:
通过传入组件渲染,渲染时会调用 React.createElement 来生成 React 元素,适用于大部分场景。
通过传入对应的 render 函数渲染,render 函数需要返回一个 React 元素。
与 render 类似,也是传入需要返回一个 React 元素的函数,区别是不管路径是否匹配,传入的 children 都会渲染。

注意:三种方法不要在同一个 组件内使用。

三种渲染方法都会传入以下三个参数:
– match
– location
– history

component

访问地址与路由匹配时,一个组件才会被渲染,此时这个组件接受 route props(match, location, history)。
当使用 component 时,router 将通过 React.createElement 去根据给定的 component 创建一个新的 react 元素。这意味你如果给 component 一个内联函数(inline function),那么每次渲染的时候都会生成一个新的组件,这会导致已经生成的组件不会挂载(unmounting),而是又重新挂载(mounting)了一个新组件。而我们想要的结果,却是去更新那个已经存在的组件。因此当你想要使用内联函数进行行内渲染(inline render)的时候,推荐使用 render 或者 children。

const Log = ({match, location, history}) => (
  <div>我是日志</div>
)
<Route component={Log} path='/log' />

render: func

render允许内联渲染和二次封装,而不会出现上文中的重复挂载问题。其间传递的参数是一样的。

// 内联渲染
<Route path="/home" render={() => <div>Home</div>}/>

// 二次封装一个fadeIn路由
const FadingRoute = ({ component: Component, ...rest }) => (
  <Route {...rest} render={props => (
    <FadeIn>
      <Component {...props}/>
    </FadeIn>
  )}/>
)

<FadingRoute path="/cool" component={Something}/>

children: func

有时候你只是想改变下其他东西,比如一个导航改变下选中态,而不是重新渲染页面,此时可以选择用 children。

<ul>
  <ListItemLink to="/somewhere" />
  <ListItemLink to="/somewhere-else" />
</ul>;

// 改变样式
const ListItemLink = ({ to, ...rest }) => (
  <Route
    path={to}
    children={({ match }) => (
      <li className={match ? "active" : ""}>
        <Link to={to} {...rest} />
      </li>
    )}
  />
);

// Animate动画组件总是被重新渲染,保证动画的执行。
<Route children={({ match, ...rest }) => (
  <Animate>
    {match && <Something {...rest}/>}
  </Animate>
)}/>

关于component,render,children 的示例代码可以看 demo3 下的 /src/App.js

path: string | string[]

任何可以被 path-to-regexp 解析的 URL。
注:如果没写 path 参数且不在 里面将会被全部匹配。

exact: bool

当 exact 为 true 的时候,路由需要准确地匹配,如“/one”无法匹配“/one/two”。反之亦然。

strict: bool

当 strict 为 true 的时候,路由后面的斜杠将被严格匹配,如“/one/”无法匹配“/one”,但是可以匹配“/one/”或者“/one/two”。反之亦然。

sensitive: bool

当 sensitive 为 true 的时候,会对大小写敏感,如“/One”无法匹配“/one”。反之亦然。
因此,当你想要严格匹配你的路由的时候,需要把 exact 和 strict 还有 sensitive 都设置为 true。

组件包裹 组件,将会它们中渲染第一个匹配的。
例如:

{// 导航放在外面,所有页面都会渲染}
<Route component={Nav} />
<Switch> 
  {// 放在里面的只会渲染一个}
  <Route component={Home} path='/' exact /> 
  <Route component={Content} path='/content' />
  <Route component={Log} path='/log' /> 
  <Route component={Other} path='/other' />
  <Route render={() => {
    return (
      <div>我是render</div>
      )
  }} path='/:render' />
</Switch>

当你访问 “/content” 的时候,若没有 则“/:render”页面也会被渲染,而加上它,则不会被渲染了。至于为什么要有此类设计,因为很多元素像是导航之类的,是所有页面共有的,则可以放在 外面匹配所有。关于它的示例代码在 demo3

组件的特殊版本,说白了就是导航。

activeClassName: string

激活态下的 class 名,默认是 activite。

activeStyle: object

激活状态下的样式。

exact: bool

若为true,则严格匹配路由,类似 的exact属性。

strict: bool

若为true,则严格匹配“/”,类似 的strict属性

isActive: func

此属性可以添加额外的逻辑来确定路由是否激活,如果除了验证链接与当前 URL 匹配之外,还想执行更多操作,则可以使用这个属性。

使用 组件,将会重定向到一个新的地址,并且新地址覆盖现有地址在访问历史里的记录,就是说无法回退至现有地址。例如我们可以用这个组件来做登录跳转:

{// isLogn 为 false 的时候,直接触发跳转登录页}
<Route path='/' render={() => (
  isLogin 
  ? 
  (<Home />)
  :
  (<Redirect to='/login' />)
)} exact />

to: string | object

重定向的地址,可以被 path-to-regexp 解析。

push: bool

若为 true,将会把地址推入历史记录中,而不是替换现有地址。

from: string

需要匹配的现有地址。官方建议放在 里面使用,保证匹配的唯一性。

exact: bool,strict: bool

的相关属性类似。

组件用于在离开页面时,做一些提示。

message: string | func

离开页面时需要提示的一些信息。

when: bool

离开页面时是否要提示。

React Router 三大参数对象

从上文可以看到,在很多地方都提及了 React Router 的三个参数对象:history,location,match,这三个对象可以在运行 demo 的时候打印下来看一下,下面将详细介绍这三个对象的作用和用法。

history

history 是 React Router 的两大重要依赖(另一个是 React)之一,它在不同的 js 环境中都实现了对会话历史的管理。
如今有以下几种方式会被用到:
– “browser history” ——用于支持 history HTML5 API 的浏览器,需要有 DOM 支持。
– “hash history”——用于旧版的浏览器,也需要有DOM支持。
– “memory history”——内存中历史记录的实现,通常用于无DOM环境,比如测试环境,或者 React Native 等。
history 对象通常有以下几种属性和方法:
– length – (number) 浏览历史堆栈中的数量
– action – (string) 跳转到当前页面执行的动作(PUSH,REPLACE 或者 POP)
– location – (object) 当前页面的 location 对象
– location.pathname – (string) URL路径
– location.search – (string) URL的字符参数
– location.hash – (string) URL中的hash
– location.state – (object) 例如在执行 push(path, state) 操作时,state会被记录到历史记录堆栈中。只适用于browser history 和 memory history
– push(path, [state]) – (function) 在历史堆栈中加入新的条目
– replace(path, [state]) – (function) 替换历史堆栈中当前的条目
– go(n) – (function) 历史堆栈中的指针向前移动n,页面发生前进操作
– goBack() – (function) 相当于go(-1),返回上一页
– goForward() – (function) 相当于go(1),前进一页
– block(prompt) – (function) 阻止跳转

{
  action: "PUSH",
  block: ƒ block(),
  createHref: ƒ createHref(location),
  go: ƒ go(n),
  goBack: ƒ goBack(),
  goForward: ƒ goForward(),
  length: 5,
  listen: ƒ listen(listener),
  location:
  {
    hash: "#world",
    key: "7ickpi3i926",
    pathname: "/content",
    search: "?hello",
    state: {name: "测试员1"}
  },
  push: ƒ push(path, state),
  replace: ƒ replace(path, state)
}

history 是易变的

history 是易变的,因此当你需要 location 对象时,要去直接取 location 参数,比如 中,而不去拿 history.location 来用。引用官方示例:

class Comp extends React.Component {
  componentDidUpdate(prevProps) {
    // 结果为 true
    const locationChanged = this.props.location !== prevProps.location;

    // 通常结果都为 false 就因为 history 的可变性
    const locationChanged =
      this.props.history.location !== prevProps.history.location;
  }
}

<Route component={Comp} />;

location

location 为你展示了当前页面从哪儿来,到哪里去,以及现在是什么状态。它主要有以下几个属性:
key – (string) 随机字符串,作为 location 的id,HashHistory 没有这个属性
– pathname – (string) 当前页面 URL
– search – (string) 当前 URL 的字符串参数
– hash – (string) 当前 URL 的 hash
– state – (object) 你要传递的 state 参数

{
  key: 'ac3df4', // HashHistory 没有!
  pathname: '/somewhere'
  search: '?some=search-string',
  hash: '#howdy',
  state: {
    [userDefined]: true
  }
}

location 通常会在下面场景中出现:
– Route component – this.props.location
– Route render – ({ location }) => ()
– Route children – ({ location }) => ()
– withRouter – this.props.location
history.location 也算是 location 的一种应用场景但是不要用它,因为 history 是可变的。
location 对象是在生命周期内,是不变的。因此你可以通过 location 来确定页面 URL 的改变,在动画以及拉取数据时常会用到。

componentWillReceiveProps(nextProps) {
  if (nextProps.location !== this.props.location) {
    // 组件更新过程中 location 改变了,证明页面URL发生了变化
  }
}
  • location 通常可以在以下情景中使用:
  • web 端的 Link 类组件。
  • React Native 端的 Link 类组件。
  • < Redirect to={location} />
  • history.push(location)
  • history.replace(location)
    location 可以让你不再只是根据 URL 的字符串来做一些逻辑,可以通过它的 state 等属性,来更轻松地完成你的逻辑。

match

math对象里面存储了 与 URL 匹配的信息,主要包含:
params – (object) URL 动态匹配的参数,如 URL 为 “/user/:name”,你访问 “/user/chenxin”,params为:{ name: ‘chenxin’ }
isExact – (boolean) URL是否是严格匹配
path – (string) 用来创建嵌套的 的匹配字段
url – (string) 用来创建嵌套的 的匹配字段

{
  isExact: true,
  params: { name: "chenxin" },
  path: "/user/:name",
  url: "/user/chenxin"
}

match通常在以下场景中使用:
– Route component 中 this.props.match
– < Route render={ ({ match }) => () }/>
– < Route children={ ({ match }) => () }/>
– widthRouter 中 this.props.match
– matchPath 的返回值
如上,是关于 React Router 在 web 端开发中,一些常见配置项的介绍。
参考文献:React Router 官方文档
原文地址:https://www.yuque.com/fe9/basic/tolg7o#pt3mtd
本文是我写于《前端九部-入门者手册2019》中的文章,本手册是一群前端爱好者们合写的新手入门手册,欢迎围观。

React Router 简介

前言

前端路由是一款 SPA 应用的核心概念之一,它通过监听 url 的改变,在不重载应用的情况下,渲染出不同的页面,目前主流的两种路由分别是:
利用 hashchange 事件来监听 url 的 hash 变化,其兼容性较好。
调用 history 的 H5 API(pushState, replaceState等)能够在不刷新页面的情况下改变 url,利用 popstate 事件可以监听到这种改变,兼容性略差,但是url风格可以与服务端基本保持一致,且可以隐性的传参,目前比较推荐用此类路由。具体原理可以参考相关文档。

React Router 是 react 官方推荐的一款路由库。它遵循 react 万物皆组件的理念,声明式(你不需要知道它怎么做,而只需要告诉它怎么做)地控制路由跳转并渲染出指定的页面,而不需要去重载整个应用。

React Router 简介

目前 React Router 已经更新至V4.x版本,本文也主要围绕此版本来做相关介绍,其他版本可参考官方文档:
React Router V2.x
React Router V3.x

React Router V4.x较之前版本做了较大的改动,其按单代码仓库模型(monorepo)来进行代码规划,打开它的 github 查看其 packages 目录,可以发现React Router分为以下几个独立的部分:
核心部分 react-router
绑定了 DOM 操作的 react-router-dom(常用于 web 应用)
用在 React Native 上的 react-router-native(用于 native App)
用于配置静态路由的 react-router-config

monorepo 的好处就是,你只需要按照自己的需求,用 npm 安装这四个中的一个即可。本文大部分示例使用的都是 react-router-dom,它与 react-router 的区别是多了很多 DOM 类组件(如 等)。

安装与使用

你可以使用 react 官方提供的脚手架 create-react-app 来快速启动一个 react 应用,并且安装 react-router,我们这里是构建 webapp,所以使用的 react-router-dom。

Bash
# 全局安装create-react-app脚手架
npm install -g create-react-app
# 建立一个react项目
create-react-app react-router-demo
# 进入项目目录
cd react-router-demo
# 安装react-router-dom
npm install --save react-router-dom

然后打开 ./src/App.js,修改其中代码为:

import React, { Component } from 'react';
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';
/* 
** 此处定义的组件,应为业务逻辑中的容器(smart)组件;
** 可以通过如import Home from './containers/Home/Home'来引入;
** 为了快速展示demo,顾简单定义了一下。
*/
const Home = () => (
  <div>我是主页</div>
)
const Content = () => (
  <div>我是内容</div>
)
const Log = () => (
  <div>我是日志</div>
)
const Nav = () => (
  <div>
    <Link to='/home'>
    主页
    </Link>
    <Link to='/content'>
    内容
    </Link>
    <Link to='/log'>
    日志
    </Link>
  </div>
)
class App extends Component {
  render() {
    return (
      <div className="App">
        <BrowserRouter>
          <Switch> 
            <Route component={Nav} path='/' exact />
            <Route component={Home} path='/home' />
            <Route component={Content} path='/content' />
            <Route component={Log} path='/log' />
          </Switch>
        </BrowserRouter>
      </div>
    );
  }
}


export default App;

至此,一个简单的 demo 就生成了。具体代码可以查看 demo1

原文地址:https://www.yuque.com/fe9/basic/kng36q
本文是我写于《前端九部-入门者手册2019》中的文章,本手册是一群前端爱好者们合写的新手入门手册,欢迎围观。

ES6 CLASS语法糖与ES5的对应关系

1.前言

es6中,引入了class的概念,让我们在用JS实现类的操作的时候更加的顺手,具体使用方法如下:

class a {
    constructor() {
        this.name = 'me';
    }
    sayHello() {
        console.log('hello world');
    }
}
var b = new a();
console.log(b.name); // me
b.sayHello() // hello world

但实际上,class只是把我们es5构建构造函数的prototype等隐藏起来了,由以下操作:

typeof a === 'function'

可以看出,其实class只是我们es5构造函数的一种语法糖。但是这种写法,更符合面向对象编程的习惯。以上例子用es5写出:

function a () {
    this.name = 'me';
}
a.prototype.sayHello = function () {
    console.log('hello world');
}

2.静态方法static

es6中的class提出了静态方法static的概念,静态方法不会挂载到这个类所构建的实例上:

class a {
    constructor() {
        this.name = 'me';
    }
    sayHello() {
        console.log('hello world');
    }
    static sayHi() {
        console.log('hi');
    }
}
var b = new a();
console.log(b.name); // me
b.sayHello() // hello world
b.sayHi() // Uncaught TypeError: b.sayHi is not a function
a.sayHi() // hi

当实例b调用sayHi()时会报错,其es5实现如下:

function a () {
    this.name = 'me';
}
a.prototype.sayHello = function () {
    console.log('hello world');
}
a.prototype.constructor.sayHi = function () {
    console.log('hi');
}

由于构造函数原型prototype的constructor指向其本身,向其中添加方法就不会被其实例访问到了。

3.继承

es6的class同样有一套非常优雅的继承写法,如下:

class parent {
    constructor() {
        this.name = 'parent and son';
        this.id = 'parent';
    }
    sayHello() {
        console.log('hello world');
    }
    static sayHi() {
        console.log('hi');
    }
}
class son extends parent {
    constructor() {
        super();
        this.id = 'son';
        this.subName = 'son and parent';
    }
    showId() {
        console.log(this.id);
    }
}
var t = new son();
t.id; // son
t.subName; // son and parent
t.name; // parent and son
t.sayHello() // hello world
t.showId() // son
t.sayHi() // Uncaught TypeError: t.sayHi is not a function

子类son继承了父类parent的name和id属性,同时也继承了sayHello方法,同时又有自己的属性subName和showId。可以看到在继承的时候,子类son在constructor中需要执行super()才能调用this,那是因为在子类constructor的时候this还没生成,super()的作用是将父类实例化然后再与子类的属性和方法一起加工成子类的this,类似于parent.prototype.constructor.call(this, props),其es5实质上就是一个组合继承:

// 父类
function parent () {
    this.name = 'parent and son';
    this.id = 'parent';
}
parent.prototype.sayHello = function () {
    console.log('hello world')
}
parent.prototype.constructor.sayHi = function() {
    console.log('hi');
}
// 子类
function son () {
    parent.call(this);
    this.id = 'son';
    this.subName = 'son and parent';
}
son.prototype = new parent();
// 因为直接字面量定义了son.prototype
// son.prototype.constructor === parent
// 因此要将其重新指回son
son.prototype.constructor = son;
son.prototype.showId = function () {
    console.log(this.id);
}

4. 通用的编译方案

es5中有个函数叫Object.defineProerty,我们可以用这个属性创建一个公用的方法来编译es6中的class。(来自于class的编译参考)

var _createClass = (function() {
    function defineProperties(target, props) {
        // 遍历要放入的属性和方法
        for (var i = 0; i < props.length; i++) {
            // 拿出每一项
            var descriptor = props[i];
            // 属性或方法是否可以枚举
            descriptor.enumerable = descriptor.enumerable || false;
            // 是否能被delete删除
            descriptor.configurable = true;
            // 如果有值的则可以修改
            if ("value" in descriptor) descriptor.writable = true;
            // 设置对应属性
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }
    // 生成构造函数的核心
    return function(Constructor, protoProps, staticProps) {
        // 如果是要继承的属性则放到prototype中
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        // 如果是静态属性则放在自身
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
})();
function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}
// 例子
class test {
    constructor() {
        this.name = 'test'
    }
    sayHello() {
        console.log('hello world');
    }
    static sayHi() {
        console.log('hi')
    }
}
// 用上述方法来生成
var otest = (function () {
    function otest () {
        _classCallCheck(this, otest);

        this.name = 'test';
    }
    _createClass(otest, [{
        key: 'sayHello',
        value: function sayHello () {
            console.log('hello world');
        }
    }], [{
        key: 'sayHi',
        value: function sayHi () {
            console.log('hi')
        }
    }])
    return otest;
})()

如何获取图片的主色、辅色以及颜色统计

最近遇到一个不太常见的需求,希望我一个页面图片的背景色,能够随着图片的变化而变化,而且看上去要和谐。和谐这个词就比较抽象了,思索再三,也许使用图片的主色调,就会使得整个页面比较和谐了。而前端的canvas正好可以满足我们的需求。

获取图片数据

对于位图而言,图片是由一个个像素点组成的,而像素数据则由红、绿、蓝和alpha分量的位数四个元素组成。通过cavas的getImageData()方法,恰好可以获取图片的像素数据,具体数据格式为{data: [r, g, b, a, r, g, b, a…]}。

let imgObj = document.getElementById('yourId');

// 创建画布
let canvas = document.createElement('canvas');
canvas.setAttribute('width', imgObj.width);
canvas.setAttribute('height', imgObj.height);
let context = canvas.getContext('2d');
// 将图片画在画布上
context.drawImage(imgObj, 0, 0);
// 获取像素数据
let imgData = context.getImageData(0, 0, imgObj.width, imgObj.height);
let pixelData = imgData.data;

对图片数据进行处理

此时获取到了像素数据,因像素数据的特性,可以对这个数组进行分组,rgba四个为一组。而且在分组的同时,可以考虑对重复出现的颜色进行计数了。我们可以构建如下数据格式来完成分组与计数的需求。

// '像素数据': 数量
const COLORLIST = {
    'rgba1': 10,
    'rgba2': 22,
    ...
}

主要代码如下

let colorList = {};
let rgba = [];
let rgbaSt = '';

// 分组循环
for (let i = 0; i < pixelData.length; i += 4) {
    rgba[0] = pixelData[i];
    rgba[1] = pixelData[i + 1];
    rgba[2] = pixelData[i + 2];
    // 如果主色只要r,g,b不要a,请注释掉这句
    rgba[3] = pixelData[i + 3];

    // 避免undefined数据和alpha为0的数据
    if (rgba.indexOf(undefined) !== -1 || pixelData[i + 3] === 0) {
        continue;
    }
    // 转为字符串
    rgbaSt = rgba.join(',');

    if (rgbaSt in colorList) {
        ++colorList[rgbaSt];
    } else {
        colorList[rgbaSt] = 1;
    }
}

对颜色列表进行排序处理

至此已经获得了颜色的统计数据,我们只需要进行一下排序,就大功告成了!

let arr = [];

for (let prop in colorList) {
    arr.push({
        // 如果只获取rgb,则为`rgb(${prop})`
        color: `rgba(${prop})`,
        count: colorList[prop]
    });
}
// 数组排序
arr.sort((a, b) {
        return b.count - a.count;
});

好了,大功告成,arr就是我们排好顺序的颜色列表啦,你可以随便取里面你想取的颜色了。arr[0]和arr[1]肯定是我们需要的主辅色了。

注意:要考虑cavans处理的图片的跨域问题。
1.添加属性crossOrigin=”anonymous”。
2.转为base64

关于vue源码中发布订阅模式的探索

数据响应

开始

首先,我找到了实例instance文件夹下的vue.js。

// src/instance/vue.js
function Vue (options) {
  //初始化函数 跳转至./internal/init.js
  this._init(options)
}

显然vue的构造函数用一个this._init就解决了。接下来找到./internal/init.js,前面进行了一系列的参数初始化,之后有以下代码

// src/instance/internal/init.js
...
    // set ref
    this._updateRef()

    // initialize data as empty object.
    // it will be filled up in _initData().
    this._data = {}

    // call init hook
    this._callHook('init')

    // initialize data observation and scope inheritance.
    //初始化观察者模式数据 跳转至./state.js
    this._initState()

    // setup event system and option events.
    this._initEvents()

    // call created hook
    this._callHook('created')

    // if `el` option is passed, start compilation.
    if (options.el) {
      this.$mount(options.el)
    }
...

我在this._initState()这个函数中找到了数据响应初始化的相关内容。跳转至./state.js

// src/instance/internal/state.js

  /**
   * Initialize the data.
   */

  Vue.prototype._initData = function () {
    // this.$options.data: 实例中的data() {return ...};
    var dataFn = this.$options.data
    var data = this._data = dataFn ? dataFn() : {}
    // isPlainObject: 来自../../util/lang;
    //判断是否为对象
    if (!isPlainObject(data)) {
      data = {}
      //这个就是data声明出错的警告了
      process.env.NODE_ENV !== 'production' && warn(
        'data functions should return an object.',
        this
      )
    }
    var props = this._props
    // proxy data on instance
    var keys = Object.keys(data)
    var i, key
    i = keys.length
    while (i--) {
      key = keys[i]
      // there are two scenarios where we can proxy a data key:
      // 1. it's not already defined as a prop
      // 2. it's provided via a instantiation option AND there are no
      //    template prop present
      //1.将data属性直接代理到vm上去,这样就可以直接访问属性了。
      //2.比如访问实例化vm对象data下的a属性,直接vm.a即可。
      if (!props || !hasOwn(props, key)) {
        //若不是props里的属性,则直接代理。
        //_proxy方法源码在下面,作用是vm直接访问_data里面的数据
        this._proxy(key)
      } else if (process.env.NODE_ENV !== 'production') {
        //发出警告。
        warn(
          'Data field "' + key + '" is already defined ' +
          'as a prop. To provide default value for a prop, use the "default" ' +
          'prop option; if you want to pass prop values to an instantiation ' +
          'call, use the "propsData" option.',
          this
        )
      }
    }
    // observe data
    //生成观察者模式的对象,跳转至../../observer/index
    observe(data, this)
  }

这个函数解决了我平时使用vue的几个疑问:

(1).无论是手抖还是什么原因。经常出现的几个警告和报错原来来自这里。
(2).为什么我明明写在vue data属性下的数据,可以直接通过vm.xxx来访问了。

第二个问题,主要依靠_proxy这个函数,将_data里的数据直接挂在到了vm下面。


/** * Proxy a property, so that * vm.prop === vm._data.prop * * @param {String} key */ Vue.prototype._proxy = function (key) { if (!isReserved(key)) { // need to store ref to self here // because these getter/setters might // be called by child scopes via // prototype inheritance. var self = this Object.defineProperty(self, key, { configurable: true, enumerable: true, get: function proxyGetter () { return self._data[key] }, set: function proxySetter (val) { self._data[key] = val } }) } }

然后_initData最后一句,observe(data, this)进入正菜

observe

// src/observer/index.js

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 *
 * @param {*} value
 * @param {Vue} [vm]
 * @return {Observer|undefined}
 * @static
 */

export function observe (value, vm) {
  if (!value || typeof value !== 'object') {
    //判断是否为对象
    return
  }
  var ob
  if (
    //__ob__这个属性若存在,证明已经observe过。直接赋值ob
    hasOwn(value, '__ob__') &&
    value.__ob__ instanceof Observer
  ) {
    ob = value.__ob__
  } else if (
    //shouldConvert:开关,暂不知道其作用
    //Object.isExtensible:判断该对象是否可以添加新属性
    shouldConvert &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    //构造函数Observer,看返回值。
    ob = new Observer(value)
  }
  if (ob && vm) {
    //vm: 当前实例
    //addVm:将vm添加到ob的vms属性中
    ob.addVm(vm)
  }
  //最后输出的ob应是一个可以get和set的对象
  return ob
}

很明显,observe这个函数,就是用来生成一个经过处理的,可以get和set的对象,所以有如下问题:

(1)get和set是什么?
(2)这么做的思路是什么?

问题1:js有个很神奇的方法,Object.defineProperty,用来设置对象的一些属性。其中就有get和set,照英文表面意思,一个‘获得’,一个’设置‘。
问题2:通过get,用来收集依赖于此条数据的订阅者。何为订阅者,比如此条数据为{ss: 1},某个dom展示{{ss}}即为它的订阅者,而通过set,当数据发生改变的时候,通知对应的订阅者。
如此就可以实现vue传说中的订阅功能。它的具体实现在Dep(dependence简写)这个构造函数中(src/observer/dep.js)

Observer

在observe这个函数中有一个构造函数Observer,是用来生成这种对象的核心函数。

// src/observer/index.js

/**
 * Observer class that are attached to each observed
 * object. Once attached, the observer converts target
 * object's property keys into getter/setters that
 * collect dependencies and dispatches updates.
 *
 * @param {Array|Object} value
 * @constructor
 */

export function Observer (value) {
  //将传参value挂载到this上
  this.value = value
  //将依赖收集对象实例化,挂载到this上
  //构造函数Dep跳转至./dep
  this.dep = new Dep()
  //Object.definePropert封装../util/lang
  //将Observer实例化后的对象挂载到value.__ob__下
  def(value, '__ob__', this)
  if (isArray(value)) {
    //由于数组的特殊性,若用Object.defineProperty,存在以下问题:
    //1.将数字作为属性存在性能问题
    //2.无法解决push pop等数组方法
    //解决方案:
    //重写数组方法,由于es5继承数组方法是返回新数组,所以得用特别的继承方式
    //(1):利用大部分高级浏览器的__proto__属性,指向Array.prototype里的方法
    //(2):遍历,将方法def到数组实例上
    //hasProto:from ../util/env
    //protoAugment:方案(1)
    //copyAugment:方案(2)
    //arrayMethods:重写方法 from ./array
    var augment = hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    //walk:遍历
    this.walk(value)
  }
}

从代码逻辑上看,这个函数解决了数组这个特殊的Object所产生的问题。数组使用Object.defineProperty存在以下问题:

1.将数字作为属性存在性能问题
2.无法准确响应push pop等数组方法

重写数组方法,由于es5继承数组方法是返回新数组,所以得用特别的继承方式

(1):利用大部分高级浏览器的proto属性,指向Array.prototype里的方法
(2):遍历,将方法def到数组实例上(def是作者封装的一个方法,作用是把方法挂载到相应对象下)

所以,原来我们用的push和pop等,都是变异过的。具体改写在src/observer/array.js。
接下来,无论数组还是对象,最后都要对其设置getter和setter,其核心函数为这个:

/**
 * Define a reactive property on an Object.
 *
 * @param {Object} obj
 * @param {String} key
 * @param {*} val
 */

export function defineReactive (obj, key, val) {
  //dep:阅读完本段代码跳至./dep,是一个收集watcher的构造函数。很多资料中称之为依赖收集
  var dep = new Dep()
  //Object.getOwnPropertyDescriptor:获取属性描述符,如writable等
  var property = Object.getOwnPropertyDescriptor(obj, key)
  //configurable:false不可更改与扩展
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get
  var setter = property && property.set

  var childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      //判断value值是否有getter
      var value = getter ? getter.call(obj) : val
      if (Dep.target) {
        //当watcher发生变化时,触发所有依赖的getter
        //然后存储依赖于此的订阅者dep.depend()相当于dep.addSub(Dep.target)
        //这样watcher成功的分发到了相关依赖中。
        //可以理解为解决getter不能传参但需要传参的情况
        dep.depend()
        if (childOb) {
          //Observer构造函数中已经声明this.dep = new Dep();
          childOb.dep.depend()
        }
        if (isArray(value)) {
          for (var e, i = 0, l = value.length; i < l; i++) {
            e = value[i]
            e && e.__ob__ && e.__ob__.dep.depend()
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val
      if (newVal === value) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = observe(newVal)
      //通知更新
      dep.notify()
    }
  })
}

其中的getter,主要依靠Dep这个依赖收集器,来收集依赖于此条字段的那些订阅者。并且存储到dep.subs这个数组中。个人认为如果getter能传参,就不需要如此大费周章了,可惜现实是不能。但这也是此段代码的神奇之处,真的很神奇!
然后setter,来判断数据是否发生了改变,并且通知更新,dep.notify()。
所以,所有的问题都指向了Dep这个构造函数。

Dep(src/observer/dep.js)

Dep这个类其实很简单,基本上就是实现watcher实例的增删改查。然后将其存储在subs这个数组中。其中Dep.target指向的是当前watcher,在afterwatch后,会重置为null。
至于watcher,是我接下来学习的对象。

 

rn踩坑纪

1.

Cannot find entry file index.ios.js in any of the roots:["/Users/xxx/Desktop/AwesomeProject"]

该问题一般都是因为当前已经开启某个项目的react-native server,所以只要关闭然后重新运行新项目就可以了。


2.

** BUILD FAILED **



The following commands produced analyzer issues:
    Analyze /Users/scdzs5/wh/project/hpApp/node_modules/react-native/ReactCommon/yoga/yoga/YGNodeList.c
    Analyze /Users/scdzs5/wh/project/hpApp/node_modules/react-native/ReactCommon/yoga/yoga/Yoga.c
(2 commands with analyzer issues)

The following build commands failed:
    PhaseScriptExecution Install\ Third\ Party /Users/scdzs5/wh/project/hpApp/ios/build/Build/Intermediates/React.build/Debug-iphonesimulator/double-conversion.build/Script-190EE32F1E6A43DE00A8543A.sh
(1 failure)

一般是版本升级不兼容导致的,reactive-native upgrade手动升级

安装ruby

# Ubuntu系统下安装ruby/rails必要的库和编译环境
sudo apt-get update
sudo apt-get install -y build-essential openssl curl libcurl3-dev libreadline6 libreadline6-dev git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libxml2-dev libxslt-dev autoconf automake libtool imagemagick libmagickwand-dev libpcre3-dev libsqlite3-dev
# rbenv环境安装
git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc
source ~/.zshrc
type rbenv
git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
# ruby环境安装,首先列出可安装的版本,然后选择后进行下载编译
rbenv install -l
rbenv install 2.2.3
# 设置当前使用的ruby版本并将gem的源改为淘宝镜像
rbenv global 2.2.3
rbenv rehash
gem sources --remove https://rubygems.org/
gem sources -a http://ruby.taobao.org/
# 安装rails
gem install bundler rails --no-rdoc --no-ri
# 检查安装后的软件版本
ruby -v
gem -v
rake -V
rails -v

【转载】关于启用 HTTPS 的一些经验分享

HTTPS 网页中加载的 HTTP 资源被称之为 Mixed Content(混合内容),不同浏览器对 Mixed Content 有不一样的处理规则。

作者:来源:Jerry Qu的小站|2015-12-04 10:04

随着国内网络环境的持续恶化,各种篡改和劫持层出不穷,越来越多的网站选择了全站 HTTPS。HTTPS 通过 TLS 层和证书机制提供了内容加密、身份认证和数据完整性三大功能,可以有效防止数据被查看或篡改,以及防止中间人冒充。本文分享一些启用 HTTPS 过程中的经验,重点是如何与一些新出的安全规范配合使用。至于 HTTPS 的部署及优化,之前写过很多,本文不重复了。

理解 Mixed Content

HTTPS 网页中加载的 HTTP 资源被称之为 Mixed Content(混合内容),不同浏览器对 Mixed Content 有不一样的处理规则。

早期的 IE

早期的 IE 在发现 Mixed Content 请求时,会弹出「是否只查看安全传送的网页内容?」这样一个模态对话框,一旦用户选择「是」,所有 Mixed Content 资源都不会加载;选择「否」,所有资源都加载。

比较新的 IE

比较新的 IE 将模态对话框改为页面底部的提示条,没有之前那么干扰用户。而且默认会加载图片类 Mixed Content,其它如 JavaScript、CSS 等资源还是会根据用户选择来决定是否加载。

现代浏览器

现代浏览器(Chrome、Firefox、Safari、Microsoft Edge),基本上都遵守了 W3C 的 Mixed Content 规范,将 Mixed Content 分为Optionally-blockable 和 Blockable 两类:

Optionally-blockable 类 Mixed Content 包含那些危险较小,即使被中间人篡改也无大碍的资源。现代浏览器默认会加载这类资源,同时会在控制台打印警告信息。这类资源包括:

通过 标签加载的图片(包括 SVG 图片);

  • 通过 <img> 标签加载的图片(包括 SVG 图片);
  • 通过 <video> / <audio> 和 <source> 标签加载的视频或音频;
  • 预读的(Prefetched)资源;

预读的(Prefetched)资源;

除此之外所有的 Mixed Content 都是 Blockable,浏览器必须禁止加载这类资源。所以现代浏览器中,对于 HTTPS 页面中的 JavaScript、CSS 等 HTTP 资源,一律不加载,直接在控制台打印错误信息。

移动浏览器

前面所说都是桌面浏览器的行为,移动端情况比较复杂,当前大部分移动浏览器默认都允许加载 Mixed Content。也就是说,对于移动浏览器来说,HTTPS 中的 HTTP 资源,无论是图片还是 JavaScript、CSS,默认都会加载。

一般选择了全站 HTTPS,就要避免出现 Mixed Content,页面所有资源请求都走 HTTPS 协议才能保证所有平台所有浏览器下都没有问题。

合理使用 CSP

CSP,全称是 Content Security Policy,它有非常多的指令,用来实现各种各样与页面内容安全相关的功能。

block-all-mixed-content

前面说过,对于 HTTPS 中的图片等 Optionally-blockable 类 HTTP 资源,现代浏览器默认会加载。图片类资源被劫持,通常不会有太大的问题,但也有一些风险,例如很多网页按钮是用图片实现的,中间人把这些图片改掉,也会干扰用户使用。

通过 CSP 的 block-all-mixed-content 指令,可以让页面进入对混合内容的严格检测(Strict Mixed Content Checking)模式。在这种模式下,所有非 HTTPS 资源都不允许加载。跟其它所有 CSP 规则一样,可以通过以下两种方式启用这个指令:

HTTP 响应头方式:

  1. Content-Security-Policy: block-all-mixed-content

<meta>标签方式:

  1. <meta http-equiv=“Content-Security-Policy” content=“block-all-mixed-content”>

upgrade-insecure-requests

历史悠久的大站在往 HTTPS 迁移的过程中,工作量往往非常巨大,尤其是将所有资源都替换为 HTTPS 这一步,很容易产生疏漏。即使所有代码都确认没有问题,很可能某些从数据库读取的字段中还存在 HTTP 链接。

而通过 upgrade-insecure-requests 这个 CSP 指令,可以让浏览器帮忙做这个转换。启用这个策略后,有两个变化:

页面所有 HTTP 资源,会被替换为 HTTPS 地址再发起请求;

页面所有站内链接,点击后会被替换为 HTTPS 地址再跳转;

跟其它所有 CSP 规则一样,这个指令也有两种方式来启用,具体格式请参考上一节。需要注意的是 upgrade-insecure-requests 只替换协议部分,所以只适用于 HTTP/HTTPS 域名和路径完全一致的场景。

合理使用 HSTS

在网站全站 HTTPS 后,如果用户手动敲入网站的 HTTP 地址,或者从其它地方点击了网站的 HTTP 链接,依赖于服务端 301/302 跳转才能使用 HTTPS 服务。而第一次的 HTTP 请求就有可能被劫持,导致请求无法到达服务器,从而构成 HTTPS 降级劫持。

HSTS 基本使用

这个问题可以通过 HSTS(HTTP Strict Transport Security,RFC6797)来解决。HSTS 是一个响应头,格式如下:

  1. Strict-Transport-Security: max-age=expireTime [; includeSubDomains] [; preload]

max-age,单位是秒,用来告诉浏览器在指定时间内,这个网站必须通过 HTTPS 协议来访问。也就是对于这个网站的 HTTP 地址,浏览器需要先在本地替换为 HTTPS 之后再发送请求。

includeSubDomains,可选参数,如果指定这个参数,表明这个网站所有子域名也必须通过 HTTPS 协议来访问。

preload,可选参数,后面再介绍它的作用。

HSTS 这个响应头只能用于 HTTPS 响应;网站必须使用默认的 443 端口;必须使用域名,不能是 IP。而且启用 HSTS 之后,一旦网站证书错误,用户无法选择忽略。

HSTS Preload List

可以看到 HSTS 可以很好的解决 HTTPS 降级攻击,但是对于 HSTS 生效前的首次 HTTP 请求,依然无法避免被劫持。浏览器厂商们为了解决这个问题,提出了 HSTS Preload List 方案:内置一份列表,对于列表中的域名,即使用户之前没有访问过,也会使用 HTTPS 协议;列表可以定期更新。

目前这个 Preload List 由 Google Chrome 维护,Chrome、Firefox、Safari、IE 11 和 Microsoft Edge 都在使用。如果要想把自己的域名加进这个列表,首先需要满足以下条件:

拥有合法的证书(如果使用 SHA-1 证书,过期时间必须早于 2016 年);

将所有 HTTP 流量重定向到 HTTPS;

确保所有子域名都启用了 HTTPS;

输出 HSTS 响应头:

max-age 不能低于 18 周(10886400 秒);

必须指定 includeSubdomains 参数;

必须指定 preload 参数;

即便满足了上述所有条件,也不一定能进入 HSTS Preload Lis。通过 Chrome 的 chrome://net-internals/#hsts 工具,可以查询某个网站是否在 Preload List 之中,还可以手动把某个域名加到本机 Preload List。

对于 HSTS 以及 HSTS Preload List,我的建议是只要你不能确保永远提供 HTTPS 服务,就不要启用。因为一旦 HSTS 生效,你再想把网站重定向为 HTTP,之前的老用户会被无限重定向,唯一的办法是换新域名。

CDN 安全

对于大站来说,全站迁移到 HTTPS 后还是得用 CDN,只是必须选择支持 HTTPS 的 CDN 了。如果使用第三方 CDN,安全方面有一些需要考虑的地方。

合理使用 SRI

HTTPS 可以防止数据在传输中被篡改,合法的证书也可以起到验证服务器身份的作用,但是如果 CDN 服务器被入侵,导致静态文件在服务器上被篡改,HTTPS 也无能为力。

W3C 的 SRI(Subresource Integrity)规范可以用来解决这个问题。SRI 通过在页面引用资源时指定资源的摘要签名,来实现让浏览器验证资源是否被篡改的目的。只要页面不被篡改,SRI 策略就是可靠的。

SRI 并不是 HTTPS 专用,但如果主页面被劫持,攻击者可以轻松去掉资源摘要,从而失去浏览器的 SRI 校验机制。

了解 Keyless SSL

另外一个问题是,在使用第三方 CDN 的 HTTPS 服务时,如果要使用自己的域名,需要把对应的证书私钥给第三方,这也是一件风险很高的事情。

CloudFlare 公司针对这种场景研发了 Keyless SSL 技术。你可以不把证书私钥给第三方,改为提供一台实时计算的 Key Server 即可。CDN 要用到私钥时,通过加密通道将必要的参数传给 Key Server,由 Key Server 算出结果并返回即可。整个过程中,私钥都保管在自己的 Key Server 之中,不会暴露给第三方。

CloudFlare 的这套机制已经开源,如需了解详情,可以查看他们官方博客的这篇文章:Keyless SSL: The Nitty Gritty Technical Details。

好了,本文先就写到这里,需要注意的是本文提到的 CSP、HSTS 以及 SRI 等策略都只有最新的浏览器才支持,详细的支持度可以去 CanIUse 查。切换到 HTTPS 之后,在性能优化上有很多新工作要做,这部分内容我在之前的博客中写过很多,这里不再重复,只说最重要的一点:既然都 HTTPS 了,赶紧上 HTTP/2 才是正道。