SmallW

SmallW 查看完整档案

深圳编辑中南财经政法大学  |  通信 编辑众安  |  前端 编辑 vuejs.com.cn 编辑
编辑

个人动态

SmallW 关注了专栏 · 2019-05-13

腾讯新闻前端团队

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

关注 4718

SmallW 收藏了文章 · 2019-01-06

Redux系列x:源码分析

写在前面

redux的源码很简洁,除了applyMiddleware比较绕难以理解外,大部分还是比较容易理解的。

这里假设读者对redux有一定了解,就不科普redux的概念和API啥的啦,这部分建议直接看官方文档

此外,源码解析的中文批注版已上传至github,可点击查看。本文相关示例代码,可点击查看

源码解析概览

将redux下载下来,然后看下他的目录结构。

npm install redux

这里我们需要关心的主要是src目录,源码解析需要关心的文件都在这里面了

  • index.js:redux主文件,主要对外暴露了几个核心API
  • createStore.jscreateStore 方法的定义
  • utils:各种工具方法,其中applyMiddleware、combineReducers、bindActionCreators 为redux的几个核心方法,剩余的pick、mapValue、compose为普通的工具函数
➜  src git:(master) ✗ tree
.
├── createStore.js
├── index.js
└── utils
    ├── applyMiddleware.js
    ├── bindActionCreators.js
    ├── combineReducers.js
    ├── compose.js
    ├── isPlainObject.js
    ├── mapValues.js
    └── pick.js

源码解析:index.js

超级简单,暴露了几个核心API,没了

mport createStore from './createStore';
import combineReducers from './utils/combineReducers';
import bindActionCreators from './utils/bindActionCreators';
import applyMiddleware from './utils/applyMiddleware';
import compose from './utils/compose';

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose
};

源码解析:createStore.js

直接贴上源代码,并进行简单注解。看下redux.createStore(reducer, initialState)调用的文档说明,基本就能够看懂下面代码了。

特别强调:虽然在几个文件里,createStore.js的代码行数是最多的,但却是最容易读懂的。下面几点比较关键

  1. redux.createStore(reducer, initialState) 传入了reducer、initialState,并返回一个store对象。
  2. store对象对外暴露了dispatch、getState、subscribe方法
  3. store对象通过getState() 获取内部状态
  4. initialState为 store 的初始状态,如果不传则为undefined
  5. store对象通过reducer来修改内部状态
  6. store对象创建的时候,内部会主动调用dispatch({ type: ActionTypes.INIT });来对内部状态进行初始化。通过断点或者日志打印就可以看到,store对象创建的同时,reducer就会被调用进行初始化。
import isPlainObject from './utils/isPlainObject';

/**
 * These are private action types reserved by Redux.
 * For any unknown actions, you must return the current state.
 * If the current state is undefined, you must return the initial state.
 * Do not reference these action types directly in your code.
 */
// 初始化的时候(redux.createStore(reducer, initialState)时),传的action.type 就是这货啦
export var ActionTypes = {
  INIT: '@@redux/INIT'
};

/**
 * Creates a Redux store that holds the state tree.
 * The only way to change the data in the store is to call `dispatch()` on it.
 *
 * There should only be a single store in your app. To specify how different
 * parts of the state tree respond to actions, you may combine several reducers
 * into a single reducer function by using `combineReducers`.
 *
 * @param {Function} reducer A function that returns the next state tree, given
 * the current state tree and the action to handle.
 *
 * @param {any} [initialState] The initial state. You may optionally specify it
 * to hydrate the state from the server in universal apps, or to restore a
 * previously serialized user session.
 * If you use `combineReducers` to produce the root reducer function, this must be
 * an object with the same shape as `combineReducers` keys.
 *
 * @returns {Store} A Redux store that lets you read the state, dispatch actions
 * and subscribe to changes.
 */
export default function createStore(reducer, initialState) {
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.');
  }

  var currentReducer = reducer;
  var currentState = initialState;
  var listeners = [];
  var isDispatching = false;

  /**
   * Reads the state tree managed by the store.
   *
   * @returns {any} The current state tree of your application.
   */
  // 这个方法没什么好讲的,返回当前的state
  function getState() {
    return currentState;
  }

  /**
   * Adds a change listener. It will be called any time an action is dispatched,
   * and some part of the state tree may potentially have changed. You may then
   * call `getState()` to read the current state tree inside the callback.
   *
   * @param {Function} listener A callback to be invoked on every dispatch.
   * @returns {Function} A function to remove this change listener.
   */
  // 很常见的监听函数添加方式,当store.dispatch 的时候被调用
  // store.subscribe(listener) 返回一个方法(unscribe),可以用来取消监听
  function subscribe(listener) {
    listeners.push(listener);
    var isSubscribed = true;

    return function unsubscribe() {
      if (!isSubscribed) {
        return;
      }

      isSubscribed = false;
      var index = listeners.indexOf(listener);
      listeners.splice(index, 1);
    };
  }

  /**
   * Dispatches an action. It is the only way to trigger a state change.
   *
   * The `reducer` function, used to create the store, will be called with the
   * current state tree and the given `action`. Its return value will
   * be considered the **next** state of the tree, and the change listeners
   * will be notified.
   *
   * The base implementation only supports plain object actions. If you want to
   * dispatch a Promise, an Observable, a thunk, or something else, you need to
   * wrap your store creating function into the corresponding middleware. For
   * example, see the documentation for the `redux-thunk` package. Even the
   * middleware will eventually dispatch plain object actions using this method.
   *
   * @param {Object} action A plain object representing “what changed”. It is
   * a good idea to keep actions serializable so you can record and replay user
   * sessions, or use the time travelling `redux-devtools`. An action must have
   * a `type` property which may not be `undefined`. It is a good idea to use
   * string constants for action types.
   *
   * @returns {Object} For convenience, the same action object you dispatched.
   *
   * Note that, if you use a custom middleware, it may wrap `dispatch()` to
   * return something else (for example, a Promise you can await).
   */
  // 以下情况会报错
  // 1. 传入的action不是一个对象
  // 2. 传入的action是个对象,但是action.type 是undefined
  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
        'Use custom middleware for async actions.'
      );
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
        'Have you misspelled a constant?'
      );
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.');
    }

    try {
      isDispatching = true;
      // 就是这一句啦, 将 currentState 设置为 reducer(currentState, action) 返回的值
      currentState = currentReducer(currentState, action);
    } finally {
      isDispatching = false;
    }

    // 如果有监听函数,就顺序调用
    listeners.slice().forEach(listener => listener());

    // 最后,返回传入的action
    return action;
  }

  /**
   * Replaces the reducer currently used by the store to calculate the state.
   *
   * You might need this if your app implements code splitting and you want to
   * load some of the reducers dynamically. You might also need this if you
   * implement a hot reloading mechanism for Redux.
   *
   * @param {Function} nextReducer The reducer for the store to use instead.
   * @returns {void}
   */
  function replaceReducer(nextReducer) {
    currentReducer = nextReducer;
    dispatch({ type: ActionTypes.INIT });
  }

  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  //
  // redux.createStore(reducer, initialState) 的时候, 内部会 自己调用 dispatch({ type: ActionTypes.INIT });
  // 来完成state的初始化
  dispatch({ type: ActionTypes.INIT });

  // 返回的就是这个东东了,只有四个方法
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer
  };
}

源码解析:combineReducers.js

redux.combineReducers(reducerMap) 的作用在于合并多个reducer函数,并返回一个新的reducer函数。因此可以看到,combineReducers 返回了一个函数,并且该函数的参数同样是state、reducer。

可以先看伪代码感受下,最终 store.getState() 返回的state,大概会是这么个样子{todos: xx, filter: xx}。简单的说,state被拆分成了两份,TodoReducer的返回值赋值给了state.todos,FilterReducer的返回值赋值给了state.filter

function TodoReducer(state, action) {}
function FilterReducer(state, action) {}

var finalReducers = redux.combineReducers({
    todos: TodoReducer,
    filter: FilterReducer
});

同样是直接上注解后的代码,记住几个关键就差不多了:

  1. combineReducers(reducerMap) 传入一个对象,并返回一个全新的reducer。调用方式跟跟普通的reducer一样,也是传入state、action。
  2. 通过combineReducers,对 store 的状态state进行拆分,
  3. reducerMap的key,就是 state 的key,而 调用对应的reducer返回的值,则是这个key对应的值。如上面的例子,state.todos == TodoReducer(state, action)
  4. redux.createStore(finalReducers, initialState) 调用时,同样会对 state 进行初始化。这个初始化跟通过普通的reducer进行初始化没多大区别。举例来说,如果 initialState.todos = undefined,那么 TodoReducer(state, action) 初始传入的state就是undefined;如果initialState.todos = [],那么 TodoReducer(state, action) 初始传入的state就是[];
  5. store.dispatch(action),finalReducers 里面,会遍历整个reducerMap,依次调用每个reducer,并将每个reducer返回的子state赋给state对应的key。
import { ActionTypes } from '../createStore';
import isPlainObject from '../utils/isPlainObject';
import mapValues from '../utils/mapValues';
import pick from '../utils/pick';

/* eslint-disable no-console */

function getUndefinedStateErrorMessage(key, action) {
  var actionType = action && action.type;
  var actionName = actionType && `"${actionType.toString()}"` || 'an action';

  return (
    `Reducer "${key}" returned undefined handling ${actionName}. ` +
    `To ignore an action, you must explicitly return the previous state.`
  );
}

function getUnexpectedStateKeyWarningMessage(inputState, outputState, action) {
  var reducerKeys = Object.keys(outputState);
  var argumentName = action && action.type === ActionTypes.INIT ?
    'initialState argument passed to createStore' :
    'previous state received by the reducer';

  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    );
  }

  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    );
  }

  var unexpectedKeys = Object.keys(inputState).filter(
    key => reducerKeys.indexOf(key) < 0
  );

  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    );
  }
}

// 对reducer做合法性检测
// store = Redux.createStore(reducer, initialState) -->
// currentState = initialState
// currentState = currentReducer(currentState, action);
//
// 从调用关系,调用时机来看, store.getState() 的初始值(currentState)
// 为 currentReducer(initialState, { type: ActionTypes.INIT })
//
// 1. 在初始化阶段,reducer 传入的 state 值是 undefined,此时,需要返回初始state,且初始state不能为undefined
// 2. 当传入不认识的 actionType 时, reducer(state, {type}) 返回的不能是undefined
// 3. redux/ 这个 namespace 下的action 不应该做处理,直接返回 currentState 就行 (谁运气这么差会去用这种actionType...)
function assertReducerSanity(reducers) {
  Object.keys(reducers).forEach(key => {
    var reducer = reducers[key];
    var initialState = reducer(undefined, { type: ActionTypes.INIT });

    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
        `If the state passed to the reducer is undefined, you must ` +
        `explicitly return the initial state. The initial state may ` +
        `not be undefined.`
      );
    }

    var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.');
    if (typeof reducer(undefined, { type }) === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
        `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
        `namespace. They are considered private. Instead, you must return the ` +
        `current state for any unknown actions, unless it is undefined, ` +
        `in which case you must return the initial state, regardless of the ` +
        `action type. The initial state may not be undefined.`
      );
    }
  });
}

/**
 * Turns an object whose values are different reducer functions, into a single
 * reducer function. It will call every child reducer, and gather their results
 * into a single state object, whose keys correspond to the keys of the passed
 * reducer functions.
 *
 * @param {Object} reducers An object whose values correspond to different
 * reducer functions that need to be combined into one. One handy way to obtain
 * it is to use ES6 `import * as reducers` syntax. The reducers may never return
 * undefined for any action. Instead, they should return their initial state
 * if the state passed to them was undefined, and the current state for any
 * unrecognized action.
 *
 * @returns {Function} A reducer function that invokes every reducer inside the
 * passed object, and builds a state object with the same shape.
 */

export default function combineReducers(reducers) {
  // 返回一个对象, key => value 且value是function(其实就是过滤掉非function)
  var finalReducers = pick(reducers, (val) => typeof val === 'function');
  var sanityError;

  try {
    // 对所有的子reducer 做一些合法性断言,如果没有出错再继续下面的处理
    // 合法性断言的内容,见API注释
    assertReducerSanity(finalReducers);
  } catch (e) {
    sanityError = e;
  }

  // 所有的 key: value,将value置成了undefined,费解...
  // 总而言之, 初始state 就是 类似 {hello: undefined, world: undefined} 的东东
  // TODO 确认这里的逻辑
  var defaultState = mapValues(finalReducers, () => undefined);

  return function combination(state = defaultState, action) {
    if (sanityError) {
      throw sanityError;
    }

    var hasChanged = false;
    // 这段代码,简单的说,就是循环一遍 finalState[key] = fn(reducer, key)
    var finalState = mapValues(finalReducers, (reducer, key) => {
      var previousStateForKey = state[key];
      var nextStateForKey = reducer(previousStateForKey, action);
      if (typeof nextStateForKey === 'undefined') {
        // 其他一个reducer返回的是undefined,于是挂啦...抛出错误
        var errorMessage = getUndefinedStateErrorMessage(key, action);
        throw new Error(errorMessage);
      }
      // 这段代码有些费解,从redux的设计理念上来讲,除了不认识的action type,其他情况都应该返回全新的state
      // 也就是说
      // 1. action type 认识,返回新的state,于是这里 hasChanged 为 true
      // 2. action type 不认识,返回原来的state,于是这里 hasChanged 为 false
      // 3. 不管action type 是否认识, 在原来的state上修改,但是返回的是修改后的state(没有返回拷贝),那么,hasChanged还是为false
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
      return nextStateForKey;
    });

    // 开发环境中(于是记得在生产环境去掉)
    // 后面再研究这段代码,毕竟不是主线路...
    if (process.env.NODE_ENV !== 'production') {
      var warningMessage = getUnexpectedStateKeyWarningMessage(state, finalState, action);
      if (warningMessage) {
        console.error(warningMessage);
      }
    }

    return hasChanged ? finalState : state;
  };
}

源码解析:bindActionCreator.js

别看API注释一大堆,除去合法性检查,关键代码其实就只有几句。先看个简单例子可能方便理解一些。看完之后可能会觉得,不就是对store.dispatch 的调用进行了便捷处理嘛。。。

var addTodo = function(text){
    return {
        type: 'add_todo',
        text: text
    };
};

var addTodos = function(){
    return {
        type: 'add_todos',
        items: Array.prototype.slice.call(arguments, 0)
    };
};

var reducer = function(state, action){
    switch (action.type) {
        case 'add_todo':
            return state.concat(action.text);
        case 'add_todos':
            return state.concat(action.items);
        default:
            return state;
    }
};


var store = redux.createStore(reducer, []);
// 注意,关键代码在这里
var actions = redux.bindActionCreators({
    addTodo: addTodo,
    addTodos: addTodos
}, store.dispatch);

console.log('state is: ' + store.getState());

store.dispatch({type: 'add_todo', text: '读书'});
store.dispatch({type: 'add_todos', items: ['阅读', '睡觉']});
console.log('state is: ' + store.getState());  // state is: 读书,阅读,睡觉

actions.addTodo('看电影');
console.log('state is: ' + store.getState());  // state is: 读书,阅读,睡觉,看电影

actions.addTodos(['刷牙', '洗澡']);
console.log('state is: ' + store.getState());  // state is: 读书,阅读,睡觉,看电影,刷牙,洗澡

所以,直接看代码吧,挺简单的。

import mapValues from '../utils/mapValues';

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args));
}

/**
 * Turns an object whose values are action creators, into an object with the
 * same keys, but with every function wrapped into a `dispatch` call so they
 * may be invoked directly. This is just a convenience method, as you can call
 * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
 *
 * For convenience, you can also pass a single function as the first argument,
 * and get a function in return.
 *
 * @param {Function|Object} actionCreators An object whose values are action
 * creator functions. One handy way to obtain it is to use ES6 `import * as`
 * syntax. You may also pass a single function.
 *
 * @param {Function} dispatch The `dispatch` function available on your Redux
 * store.
 *
 * @returns {Function|Object} The object mimicking the original object, but with
 * every action creator wrapped into the `dispatch` call. If you passed a
 * function as `actionCreators`, the return value will also be a single
 * function.
 */
// 假设 actionCreators === {addTodo: addTodo, removeTodo: removeTodo}
// 简单的来说 bindActionCreators(actionCreators, dispatch)
// 最后返回的是:
// {
//   addTodo: function(text){
//      dispatch( actionCreators.addTodo(text) );
//   },
//   removeTodo: function(text){
//      dispatch( actionCreators.removeTodo(text) );
//   }
// }
//
//  或者说 actionCreators === addTodo (addTodo 为 actionCreator)
//  最后返回的是
//  function() {
//     dispatch(actionCreators());
//  }
export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch);
  }

  if (typeof actionCreators !== 'object' || actionCreators === null || actionCreators === undefined) {  // eslint-disable-line no-eq-null
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` +
      `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    );
  }

  return mapValues(actionCreators, actionCreator =>
    bindActionCreator(actionCreator, dispatch)
  );
}

源码解析:applyMiddleware.js

中间件应该是redux源码里面最绕的一部分,虽然看懂后,有一种“啊~原来不过如此”的感觉,但一开始还真是看的晕头转向的,API的说明、中间件的编写、applyMiddleware的源码实现,都不是那么好理解。

在继续源码解析之前,推荐看下官方文档对于middleware的说明,链接传送门:http://camsong.github.io/redu...

虽然文档死长死长,但硬着头皮看完,还是有所收获的,终于知道 applyMiddleware 的实现这么绕了。。。

例子:redux-thunk

用redux处理过异步请求的同学应该用过redux-thunk,我们来看下他的源码,奇短无比,别说你的小伙伴了,我的小伙伴都惊呆了。

export default function thunkMiddleware({ dispatch, getState }) {
  return next => action =>
    typeof action === 'function' ?
      action(dispatch, getState) :
      next(action);
}

翻译成ES5,是这样子滴,之后你再看其他中间件的实现,其实都大同小异,下面我们写个自定义中间件,基本就可以看出点门路来。

'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = thunkMiddleware;
function thunkMiddleware(store) {
  var dispatch = store.dispatch;
  var getState = store.getState;

  return function (next) {
    return function (action) {
      return typeof action === 'function' ? action(dispatch, getState) : next(action);
    };
  };
}
module.exports = exports['default'];

自定义中间件:logger

先看logger的实现

    function middleware(store){
        return function(next){
            return function(action){
                return next(action);
            }
        }
    }

基本看出中间件声明的模版来了,就是下面这个样子。下面结合applyMiddleware的调用,来说明store、next、action 几个参数。

    function logger(store){
        return function(next){
            return function(action){
                console.log('logger: dispatching ' + action.type);
                var result = next(action);
                console.log('logger: next state ' + result);
                return result;
            }
        }
    }

applyMiddleware调用例子

完整的示例代码见本小节最后面。可以看到:

  1. applyMiddleware 的调用方式为 applyMiddleware(...middlewares)(react.createStore)。其实这里直接先创建 store,然后applyMiddleware(...middlewares)(store) 也很容易实现相同的效果,不过作者是故意这样设计的,为了避免在同一个store上多次应用同一个middlerware(参考官方文档:尝试 #6: “单纯”地使用 Middleware
  2. 中间件顶层的store参数,并不是常规的store,虽然它也有 getState、dispatch 两个方法

        // 上面的store参数,其实就是这个对象
        // 其中,store 为内部的store,我们在外面 storeWithMiddleWare.dipatch的时候,内部实现是转成 store.dispatch
        // 此外,可以看到 middlewareAPI.dispatch 方法,是最终封装后的dispatch(千万注意,如果在中间件内部 调用 store.dispatch,可能导致死循环 )
        var middlewareAPI = {
          getState: store.getState,
          // 最后面, dispatch 被覆盖, 变成包装后的 dispatch 方法
          dispatch: (action) => dispatch(action)
        };
  3. 第二层的next函数,其实是一个“dispatch”方法。熟悉express的同学大概可以猜到它的作用。storeWithMiddleWare.dispatch(action) 的时候,会顺序进入各个中间件(按照定义时的顺序)。从当前的例子来看,大约如下,其实就是柯里化啦~:
storeWithMiddleWare.dispatch(action) --> logger(store)(next)(action) --> timer(store)(next)(action) --> store.dispatch(action)

完整的示例代码

    function reducer(state, action){
        if(typeof state==='undefined') state = [];

        switch(action.type){
            case 'add_todo':
                return state.concat(action.text);
            default: 
                return state;
        }
    }
    
    function addTodo(text){
        return {
            type: 'add_todo',
            text: text
        };
    }

    // 这里的 store,并不是 redux.createStore(reducer, initialState) 出来的 store
    // 而是 {getState: store.getState, dispatch: function() { store.dispatch(action); }}
    // 
    function logger(store){    
        //     
        return function(next){
            return function(action){
                console.log('logger: dispatching ' + action.type);
                var result = next(action);
                console.log('logger: next state ' + result);
                return result;
            }
        }
    }

    function timer(store){
        return function(next){
            return function(action){
                console.log('timer: dispatching ' + action.type);
                var result = next(action);
                console.log('timer: next state ' + result);
                return result;
            }
        }
    }

    var createStoreWidthMiddleware = redux.applyMiddleware(
        logger, 
        timer
        )(redux.createStore);
    
    var storeWithMiddleWare = createStoreWidthMiddleware(reducer);
    storeWithMiddleWare.subscribe(function(){
        console.log('subscribe: state is : ' + storeWithMiddleWare.getState());
    });
    console.log( storeWithMiddleWare.dispatch(addTodo('reading')) );

源码解析

再次说下,建议先看下官方文档对中间件的介绍,不然可能会有点晕。

import compose from './compose';

/**
 * Creates a store enhancer that applies middleware to the dispatch method
 * of the Redux store. This is handy for a variety of tasks, such as expressing
 * asynchronous actions in a concise manner, or logging every action payload.
 *
 * See `redux-thunk` package as an example of the Redux middleware.
 *
 * Because middleware is potentially asynchronous, this should be the first
 * store enhancer in the composition chain.
 *
 * Note that each middleware will be given the `dispatch` and `getState` functions
 * as named arguments.
 *
 * @param {...Function} middlewares The middleware chain to be applied.
 * @returns {Function} A store enhancer applying the middleware.
 */
/*
  从调用方法 applyMiddleware(...middlewares)(Redux.createStore) 可以看出
  next 参数实际上是 Redux.createStore. 而 Redux.createStore 的调用方式为 Redux.createStore(reducer, initialState)
  所以 applyMiddleware(...middlewares)
  1. 参数: Redux.createStore
  2. 返回值:一个function, 跟 Redux.createStore 接受的参数一样

 */
export default function applyMiddleware(...middlewares) {
  return (next) => (reducer, initialState) => {
    // 内部先创建一个store (相当于直接调用 Redux.createStore(reducer, initialState))
    var store = next(reducer, initialState);
    // 保存最初始的store.dispatch
    var dispatch = store.dispatch;
    var chain = [];

    var middlewareAPI = {
      getState: store.getState,
      // 最后面, dispatch 被覆盖, 变成包装后的 dispatch 方法
      dispatch: (action) => dispatch(action)
    };
    // 返回一个数组
    // 贴个例子在这里做参考,redux-thunk
    // function thunkMiddleware(store) {
    //  var dispatch = store.dispatch;
    //  var getState = store.getState;
    //
    //  这里的next其实就是dispatch
    //  return function (next) {
    //    return function (action) {
    //      return typeof action === 'function' ? action(dispatch, getState) : next(action);
    //    };
    //  };
    //}
    /*
      chain 是个数组, 参考上面的 middlleware (redux-thunk),可以看到,chain的每个元素为如下形式的function
      并且, 传入的 store.getState 为原始的 store.getState,而 dispatch则是包装后的 dispatch(不是原始的store.dispatch)
      似乎是为了确保, 在每个middleware里调用 dispatch(action), 最终都是 用原始的 store.dispatch(action)
      避免 store.dispatch 被覆盖, 导致middleware 顺序调用的过程中, store.dispatch的值变化 --> store.dispatch 返回的值可能会有不同
      违背 redux 的设计理念

      这里的 next 则为 原始的 store.dispatch (见下面 compose(...chain)(store.dispatch) )
      function (next) {
        return function (action) {

        }
      }
     */
    chain = middlewares.map(middleware => middleware(middlewareAPI));

    // compose(...chain)(store.dispatch) 返回了一个function
    // 伪代码如下,
    // function (action) {
    //   middleware(store)(store.dispatch);
    // }
    dispatch = compose(...chain)(store.dispatch);  // 从右到左, middleware1( middleware2( middleware3(dispatch) ) )

    // 于是,最终调用 applyMiddleware(...middlewares)(Redux.createStore)
    // 返回的 store, getState,subscribe 方法都是原始的那个 store.getState, store.subscribe
    // 至于dispatch是封装过的
    return {
      ...store,
      dispatch
    };
  };
}

相关链接

官方文档:http://camsong.github.io/redu...
源码解析github地址:https://github.com/chyingp/re...
源码解析相关代码示例:https://github.com/chyingp/re...

查看原文

SmallW 赞了文章 · 2018-10-15

SVG 动画

SVG 动画

主要内容
  • SVG 是什么?
  • SVG 的一些常用用途
  • SVG 的基本结构
  • CSS 结合 SVG 生成动画

    • 圆环动画
    • logo 描边动画
  • SVG 的动画元素

SVG 是什么?

SVG ,可缩放矢量图形(Scalable Vector Graphics),是一种用来描述二维矢量图形的 XML 标记语言。 简单地说,SVG 面向图形,HTML 面向文本。

SVG 特点

  1. SVG 与 JPEG 和 GIF 图像比起来,尺寸更小,且可压缩性更强。
  2. SVG 是矢量的,可伸缩的,适用多种分辨率。
  3. SVG 是开放的标准,本质是纯粹的 XML ,可以被非常多的工具读取和修改。
  4. SVG 图像中的文本是可选的,同时也是可搜索的。

SVG 兼容性

IE9+,主流环境基本都能支持。具体参见

SVG 的一些常用用途

  • 制作iconfont
  • 作为图片文件
    img src属性引用,css background-image引用。

    SVG作为图像引用时,大多数浏览器不会加载SVG自身引用的文件(其他图像,外部脚本,字体文件等),依据浏览器的安全策略,SVG中定义的脚本也可能不会执行。
  • 作为应用程序
    SVG文件也可以作为<object>元素的data属性引入HTML中。注意,MIME type必须是image/svg+xml。

    <object data="image.svg" type="image/svg+xml">
      ...
    </object>
  • 混合文档
    可以直接将<svg>嵌入到XHTML或者HTML5文档中。
    嵌入到XHTML需要为<svg>指定命名空间(xmlns),嵌入到HTML5则可以省略,解析器会自动识别<svg>。
    在 HTML 内容中应用 SVG 效果

SVG 的基本结构

SVG 跟标签

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1024px" height="1024px">
</svg>

SVG 基本形状

1. 矩形(rect)

<rect x="60" y="10" rx="20" ry="40" width="100" height="100"/>

clipboard.png

2. 圆形(circle)

<circle cx="75" cy="75" r="60"/>

clipboard.png

3. 椭圆(ellipse)

<ellipse cx="75" cy="75" rx="30" ry="20"/>

clipboard.png

4. 线条(line)

<line x1="10" x2="50" y1="110" y2="150" stroke="black" stroke-width="3"/>

clipboard.png

5. 折线(polyline)

<polyline points="60 60, 70 120, 80 130, 70 70" style="fill: none;stroke-width: 2;stroke: black;"/>

clipboard.png

6. 多边形(polygon)

polygon和折线很像,它们都是由连接一组点集的直线构成。不同的是,polygon的路径在最后一个点处自动回到第一个点。

<polygon points="50 160, 55 180, 70 180, 60 190, 65 205, 50 195, 35 205, 40 190, 30 180, 45 180" style="fill: none;stroke: black;"/>

clipboard.png

7. 路径(point)

<path d="M 20 230 Q 40 205, 50 230 T 90230" style="fill: none;stroke: black"/>

clipboard.png

路径绘制命令

SVG常用元素还有<g>组合,常用属性fill填充,stroke线条颜色,stroke-width线条粗细等等,具体参考MDN文档。
SVG 元素参考
SVG 属性参考

CSS 结合 SVG 属性生成动画

stroke-dasharray可控制用来描边的点划线的图案范式。
stroke-dashoffset 属性指定了dash模式到路径开始的距离

圆环动画

画圆环的动画效果,可用于提示加载百分比,倒计时等场景。
https://codepen.io/wishingtre...

logo 描边动画

logo描边动画
logo描边动画2][7]

SVG 的动画元素

兼容性:ie死都不支持,移动端友好。具体参见

1.set

set元素不能产生动画效果,可以实现基本的延迟功能。就是指:可以在特定时间之后修改某个属性值(包括本身的XML属性和CSS属性值)。

2.animate

attributeName 变动的属性的属性名。
from 变动的初始值。
to 变动的终值。
dur 动画的持续时间。
举个栗子
形状补间动画

3.animateTransform

attributeName固定为transform,可针对transform的相关属性生成动画,不详细介绍了。

4.animateMotion(线性路径动画)

animateMotion 元素可以让SVG各种图形沿着特定的path路径运动。
地球围绕太阳旋转
贴合路径弧度运动

SVG动画库:
1.SVG-Morpheus
使用参考
演示参考
2.snap.svg

查看原文

赞 16 收藏 12 评论 2

SmallW 收藏了文章 · 2018-07-09

Javascript装饰器的妙用

最近新开了一个Node项目,采用TypeScript来开发,在数据库及路由管理方面用了不少的装饰器,发觉这的确是一个好东西。
装饰器是一个还处于草案中的特性,目前木有直接支持该语法的环境,但是可以通过 babel 之类的进行转换为旧语法来实现效果,所以在TypeScript中,可以放心的使用@Decorator

什么是装饰器

装饰器是对类、函数、属性之类的一种装饰,可以针对其添加一些额外的行为。
通俗的理解可以认为就是在原有代码外层包装了一层处理逻辑。
个人认为装饰器是一种解决方案,而并非是狭义的@Decorator,后者仅仅是一个语法糖罢了。

装饰器在身边的例子随处可见,一个简单的例子,水龙头上边的起泡器就是一个装饰器,在装上以后就会把空气混入水流中,掺杂很多泡泡在水里。
但是起泡器安装与否对水龙头本身并没有什么影响,即使拆掉起泡器,也会照样工作,水龙头的作用在于阀门的控制,至于水中掺不掺杂气泡则不是水龙头需要关心的。

所以,对于装饰器,可以简单地理解为是非侵入式的行为修改。

为什么要用装饰器

可能有些时候,我们会对传入参数的类型判断、对返回值的排序、过滤,对函数添加节流、防抖或其他的功能性代码,基于多个类的继承,各种各样的与函数逻辑本身无关的、重复性的代码。

函数中的作用

可以想像一下,我们有一个工具类,提供了一个获取数据的函数:

class Model1 {
  getData() {
    // 此处省略获取数据的逻辑
    return [{
      id: 1,
      name: 'Niko'
    }, {
      id: 2,
      name: 'Bellic'
    }]
  }
}

console.log(new Model1().getData())     // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]
console.log(Model1.prototype.getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]

现在我们想要添加一个功能,记录该函数执行的耗时。
因为这个函数被很多人使用,在调用方添加耗时统计逻辑是不可取的,所以我们要在Model1中进行修改:

class Model1 {
  getData() {
+   let start = new Date().valueOf()
+   try {
      // 此处省略获取数据的逻辑
      return [{
        id: 1,
        name: 'Niko'
      }, {
        id: 2,
        name: 'Bellic'
      }]
+   } finally {
+     let end = new Date().valueOf()
+     console.log(`start: ${start} end: ${end} consume: ${end - start}`)
+   }
  }
}

// start: XXX end: XXX consume: XXX
console.log(new Model1().getData())     // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]
// start: XXX end: XXX consume: XXX
console.log(Model1.prototype.getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]

这样在调用方法后我们就可以在控制台看到耗时的输出了。
但是这样直接修改原函数代码有以下几个问题:

  1. 统计耗时的相关代码与函数本身逻辑并无一点关系,影响到了对原函数本身的理解,对函数结构造成了破坏性的修改
  2. 如果后期还有更多类似的函数需要添加统计耗时的代码,在每个函数中都添加这样的代码显然是低效的,维护成本太高

所以,为了让统计耗时的逻辑变得更加灵活,我们将创建一个新的工具函数,用来包装需要设置统计耗时的函数。
通过将Class与目标函数的name传递到函数中,实现了通用的耗时统计:

function wrap(Model, key) {
  // 获取Class对应的原型
  let target = Model.prototype

  // 获取函数对应的描述符
  let descriptor = Object.getOwnPropertyDescriptor(target, key)

  // 生成新的函数,添加耗时统计逻辑
  let log = function (...arg) {
    let start = new Date().valueOf()
    try {
      return descriptor.value.apply(this, arg) // 调用之前的函数
    } finally {
      let end = new Date().valueOf()
      console.log(`start: ${start} end: ${end} consume: ${end - start}`)
    }
  }

  // 将修改后的函数重新定义到原型链上
  Object.defineProperty(target, key, {
    ...descriptor,
    value: log      // 覆盖描述符重的value
  })
}

wrap(Model1, 'getData')
wrap(Model2, 'getData')

// start: XXX end: XXX consume: XXX
console.log(new Model1().getData())     // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]
// start: XXX end: XXX consume: XXX
console.log(Model2.prototype.getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]

接下来,我们想控制其中一个Model的函数不可被其他人修改覆盖,所以要添加一些新的逻辑:

function wrap(Model, key) {
  // 获取Class对应的原型
  let target = Model.prototype

  // 获取函数对应的描述符
  let descriptor = Object.getOwnPropertyDescriptor(target, key)

  Object.defineProperty(target, key, {
    ...descriptor,
    writable: false      // 设置属性不可被修改
  })
}

wrap(Model1, 'getData')

Model1.prototype.getData = 1 // 无效

可以看出,两个wrap函数中有不少重复的地方,而修改程序行为的逻辑,实际上依赖的是Object.defineProperty中传递的三个参数。
所以,我们针对wrap在进行一次修改,将其变为一个通用类的转换:

function wrap(decorator) {
  return function (Model, key) {
    let target = Model.prototype
    let dscriptor = Object.getOwnPropertyDescriptor(target, key)

    decorator(target, key, descriptor)
  }
}

let log = function (target, key, descriptor) {
  // 将修改后的函数重新定义到原型链上
  Object.defineProperty(target, key, {
    ...descriptor,
    value: function (...arg) {
      let start = new Date().valueOf()
      try {
        return descriptor.value.apply(this, arg) // 调用之前的函数
      } finally {
        let end = new Date().valueOf()
        console.log(`start: ${start} end: ${end} consume: ${end - start}`)
      }
    }
  })
}

let seal = function (target, key, descriptor) {
  Object.defineProperty(target, key, {
    ...descriptor,
    writable: false
  })
}

// 参数的转换处理
log = wrap(log)
seal = warp(seal)

// 添加耗时统计
log(Model1, 'getData')
log(Model2, 'getData')

// 设置属性不可被修改
seal(Model1, 'getData')

到了这一步以后,我们就可以称logseal为装饰器了,可以很方便的让我们对一些函数添加行为。
而拆分出来的这些功能可以用于未来可能会有需要的地方,而不用重新开发一遍相同的逻辑。

Class 中的作用

就像上边提到了,现阶段在JS中继承多个Class是一件头疼的事情,没有直接的语法能够继承多个 Class。

class A { say () { return 1 } }
class B { hi () { return 2 } }
class C extends A, B {}        // Error
class C extends A extends B {} // Error

// 这样才是可以的
class C {}
for (let key of Object.getOwnPropertyNames(A.prototype)) {
  if (key === 'constructor') continue
  Object.defineProperty(C.prototype, key, Object.getOwnPropertyDescriptor(A.prototype, key))
}
for (let key of Object.getOwnPropertyNames(B.prototype)) {
  if (key === 'constructor') continue
  Object.defineProperty(C.prototype, key, Object.getOwnPropertyDescriptor(B.prototype, key))
}

let c = new C()
console.log(c.say(), c.hi()) // 1, 2

所以,在React中就有了一个mixin的概念,用来将多个Class的功能复制到一个新的Class上。
大致思路就是上边列出来的,但是这个mixinReact中内置的一个操作,我们可以将其转换为更接近装饰器的实现。
在不修改原Class的情况下,将其他Class的属性复制过来:

function mixin(constructor) {
  return function (...args) {
    for (let arg of args) {
      for (let key of Object.getOwnPropertyNames(arg.prototype)) {
        if (key === 'constructor') continue // 跳过构造函数
        Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key))
      }
    }
  }
}

mixin(C)(A, B)

let c = new C()
console.log(c.say(), c.hi()) // 1, 2

以上,就是装饰器在函数、Class上的实现方法(至少目前是的),但是草案中还有一颗特别甜的语法糖,也就是@Decorator了。
能够帮你省去很多繁琐的步骤来用上装饰器。

@Decorator的使用方法

草案中的装饰器、或者可以说是TS实现的装饰器,将上边的两种进一步地封装,将其拆分成为更细的装饰器应用,目前支持以下几处使用:

  1. Class
  2. 函数
  3. get set访问器
  4. 实例属性、静态函数及属性
  5. 函数参数

@Decorator的语法规定比较简单,就是通过@符号后边跟一个装饰器函数的引用:

@tag
class A { 
  @method
  hi () {}
}

function tag(constructor) {
  console.log(constructor === A) // true
}

function method(target) {
  console.log(target.constructor === A, target === A.prototype) // true, true
}

函数tagmethod会在class A定义的时候执行。

@Decorator 在 Class 中的使用

该装饰器会在class定义前调用,如果函数有返回值,则会认为是一个新的构造函数来替代之前的构造函数。

函数接收一个参数:

  1. constructor 之前的构造函数

我们可以针对原有的构造函数进行一些改造:

新增一些属性

如果想要新增一些属性之类的,有两种方案可以选择:

  1. 创建一个新的class继承自原有class,并添加属性
  2. 针对当前class进行修改

后者的适用范围更窄一些,更接近mixin的处理方式。

@name
class Person {
  sayHi() {
    console.log(`My name is: ${this.name}`)
  }
}

// 创建一个继承自Person的匿名类
// 直接返回并替换原有的构造函数
function name(constructor) {
  return class extends constructor {
    name = 'Niko'
  }
}

new Person().sayHi()

修改原有属性的描述符

@seal
class Person {
  sayHi() {}
}

function seal(constructor) {
  let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHi')
  Object.defineProperty(constructor.prototype, 'sayHi', {
    ...descriptor,
    writable: false
  })
}

Person.prototype.sayHi = 1 // 无效

使用闭包来增强装饰器的功能

在TS文档中被称为装饰器工厂

因为@符号后边跟的是一个函数的引用,所以对于mixin的实现,我们可以很轻易的使用闭包来实现:

class A { say() { return 1 } }
class B { hi() { return 2 } }

@mixin(A, B)
class C { }

function mixin(...args) {
  // 调用函数返回装饰器实际应用的函数
  return function(constructor) {
    for (let arg of args) {
      for (let key of Object.getOwnPropertyNames(arg.prototype)) {
        if (key === 'constructor') continue // 跳过构造函数
        Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key))
      }
    }
  }
}

let c = new C()
console.log(c.say(), c.hi()) // 1, 2

多个装饰器的应用

装饰器是可以同时应用多个的(不然也就失去了最初的意义)。
用法如下:

@decorator1
@decorator2
class { }

执行的顺序为decorator2 -> decorator1,离class定义最近的先执行。
可以想像成函数嵌套的形式:

decorator1(decorator2(class {}))

@Decorator 在 Class 成员中的使用

类成员上的 @Decorator 应该是应用最为广泛的一处了,函数,属性,getset访问器,这几处都可以认为是类成员。
在TS文档中被分为了Method DecoratorAccessor DecoratorProperty Decorator,实际上如出一辙。

关于这类装饰器,会接收如下三个参数:

  1. 如果装饰器挂载于静态成员上,则会返回构造函数,如果挂载于实例成员上则会返回类的原型
  2. 装饰器挂载的成员名称
  3. 成员的描述符,也就是Object.getOwnPropertyDescriptor的返回值
Property Decorator不会返回第三个参数,但是可以自己手动获取
前提是静态成员,而非实例成员,因为装饰器都是运行在类创建时,而实例成员是在实例化一个类的时候才会执行的,所以没有办法获取对应的descriptor

静态成员与实例成员在返回值上的区别

可以稍微明确一下,静态成员与实例成员的区别:

class Model {
  // 实例成员
  method1 () {}
  method2 = () => {}

  // 静态成员
  static method3 () {}
  static method4 = () => {}
}

method1method2是实例成员,method1存在于prototype之上,而method2只在实例化对象以后才有。
作为静态成员的method3method4,两者的区别在于是否可枚举描述符的设置,所以可以简单地认为,上述代码转换为ES5版本后是这样子的:

function Model () {
  // 成员仅在实例化时赋值
  this.method2 = function () {}
}

// 成员被定义在原型链上
Object.defineProperty(Model.prototype, 'method1', {
  value: function () {}, 
  writable: true, 
  enumerable: false,  // 设置不可被枚举
  configurable: true
})

// 成员被定义在构造函数上,且是默认的可被枚举
Model.method4 = function () {}

// 成员被定义在构造函数上
Object.defineProperty(Model, 'method3', {
  value: function () {}, 
  writable: true, 
  enumerable: false,  // 设置不可被枚举
  configurable: true
})

可以看出,只有method2是在实例化时才赋值的,一个不存在的属性是不会有descriptor的,所以这就是为什么TS在针对Property Decorator不传递第三个参数的原因,至于为什么静态成员也没有传递descriptor,目前没有找到合理的解释,但是如果明确的要使用,是可以手动获取的。

就像上述的示例,我们针对四个成员都添加了装饰器以后,method1method2第一个参数就是Model.prototype,而method3method4的第一个参数就是Model

class Model {
  // 实例成员
  @instance
  method1 () {}
  @instance
  method2 = () => {}

  // 静态成员
  @static
  static method3 () {}
  @static
  static method4 = () => {}
}

function instance(target) {
  console.log(target.constructor === Model)
}

function static(target) {
  console.log(target === Model)
}

函数,访问器,和属性装饰器三者之间的区别

函数

首先是函数,函数装饰器的返回值会默认作为属性的value描述符存在,如果返回值为undefined则会忽略,使用之前的descriptor引用作为函数的描述符。
所以针对我们最开始的统计耗时的逻辑可以这么来做:

class Model {
  @log1
  getData1() {}
  @log2
  getData2() {}
}

// 方案一,返回新的value描述符
function log1(tag, name, descriptor) {
  return {
    ...descriptor,
    value(...args) {
      let start = new Date().valueOf()
      try {
        return descriptor.value.apply(this, args)
      } finally {
        let end = new Date().valueOf()
        console.log(`start: ${start} end: ${end} consume: ${end - start}`)
      }
    }
  }
}

// 方案二、修改现有描述符
function log2(tag, name, descriptor) {
  let func = descriptor.value // 先获取之前的函数

  // 修改对应的value
  descriptor.value = function (...args) {
    let start = new Date().valueOf()
    try {
      return func.apply(this, args)
    } finally {
      let end = new Date().valueOf()
      console.log(`start: ${start} end: ${end} consume: ${end - start}`)
    }
  }
}

访问器

访问器就是添加有getset前缀的函数,用于控制属性的赋值及取值操作,在使用上与函数没有什么区别,甚至在返回值的处理上也没有什么区别。
只不过我们需要按照规定设置对应的get或者set描述符罢了:

class Modal {
  _name = 'Niko'

  @prefix
  get name() { return this._name }
}

function prefix(target, name, descriptor) {
  return {
    ...descriptor,
    get () {
      return `wrap_${this._name}`
    }
  }
}

console.log(new Modal().name) // wrap_Niko

属性

对于属性的装饰器,是没有返回descriptor的,并且装饰器函数的返回值也会被忽略掉,如果我们想要修改某一个静态属性,则需要自己获取descriptor

class Modal {
  @prefix
  static name1 = 'Niko'
}

function prefix(target, name) {
  let descriptor = Object.getOwnPropertyDescriptor(target, name)

  Object.defineProperty(target, name, {
    ...descriptor,
    value: `wrap_${descriptor.value}`
  })
  
  return target
}

console.log(Modal.name1) // wrap_Niko

对于一个实例的属性,则没有直接修改的方案,不过我们可以结合着一些其他装饰器来曲线救国。

比如,我们有一个类,会传入姓名和年龄作为初始化的参数,然后我们要针对这两个参数设置对应的格式校验:

const validateConf = {} // 存储校验信息

@validator
class Person {
  @validate('string')
  name
  @validate('number')
  age

  constructor(name, age) {
    this.name = name
    this.age = age
  }
}

function validator(constructor) {
  return class extends constructor {
    constructor(...args) {
      super(...args)

      // 遍历所有的校验信息进行验证
      for (let [key, type] of Object.entries(validateConf)) {
        if (typeof this[key] !== type) throw new Error(`${key} must be ${type}`)
      }
    }
  }
}

function validate(type) {
  return function (target, name, descriptor) {
    // 向全局对象中传入要校验的属性名及类型
    validateConf[name] = type
  }
}

new Person('Niko', '18')  // throw new error: [age must be number]

首先,在类上边添加装饰器@validator,然后在需要校验的两个参数上添加@validate装饰器,两个装饰器用来向一个全局对象传入信息,来记录哪些属性是需要进行校验的。
然后在validator中继承原有的类对象,并在实例化之后遍历刚才设置的所有校验信息进行验证,如果发现有类型错误的,直接抛出异常。
这个类型验证的操作对于原Class来说几乎是无感知的。

函数参数装饰器

最后,还有一个用于函数参数的装饰器,这个装饰器也是像实例属性一样的,没有办法单独使用,毕竟函数是在运行时调用的,而无论是何种装饰器,都是在声明类时(可以认为是伪编译期)调用的。

函数参数装饰器会接收三个参数:

  1. 类似上述的操作,类的原型或者类的构造函数
  2. 参数所处的函数名称
  3. 参数在函数中形参中的位置(函数签名中的第几个参数)

一个简单的示例,我们可以结合着函数装饰器来完成对函数参数的类型转换:

const parseConf = {}
class Modal {
  @parseFunc
  addOne(@parse('number') num) {
    return num + 1
  }
}

// 在函数调用前执行格式化操作
function parseFunc (target, name, descriptor) {
  return {
    ...descriptor,
    value (...arg) {
      // 获取格式化配置
      for (let [index, type] of parseConf) {
        switch (type) {
          case 'number':  arg[index] = Number(arg[index])             break
          case 'string':  arg[index] = String(arg[index])             break
          case 'boolean': arg[index] = String(arg[index]) === 'true'  break
        }
      }

      return descriptor.value.apply(this, arg)
    }
  }
}

// 向全局对象中添加对应的格式化信息
function parse(type) {
  return function (target, name, index) {
    parseConf[index] = type
  }
}

console.log(new Modal().addOne('10')) // 11

使用装饰器实现一个有趣的Koa封装

比如在写Node接口时,可能是用的koa或者express,一般来说可能要处理很多的请求参数,有来自headers的,有来自body的,甚至有来自querycookie的。
所以很有可能在router的开头数行都是这样的操作:

router.get('/', async (ctx, next) => {
  let id = ctx.query.id
  let uid = ctx.cookies.get('uid')
  let device = ctx.header['device']
})

以及如果我们有大量的接口,可能就会有大量的router.getrouter.post
以及如果要针对模块进行分类,可能还会有大量的new Router的操作。

这些代码都是与业务逻辑本身无关的,所以我们应该尽可能的简化这些代码的占比,而使用装饰器就能够帮助我们达到这个目的。

装饰器的准备

// 首先,我们要创建几个用来存储信息的全局List
export const routerList      = []
export const controllerList  = []
export const parseList       = []
export const paramList       = []

// 虽说我们要有一个能够创建Router实例的装饰器
// 但是并不会直接去创建,而是在装饰器执行的时候进行一次注册
export function Router(basename = '') {
  return (constrcutor) => {
    routerList.push({
      constrcutor,
      basename
    })
  }
}

// 然后我们在创建对应的Get Post请求监听的装饰器
// 同样的,我们并不打算去修改他的任何属性,只是为了获取函数的引用
export function Method(type) {
  return (path) => (target, name, descriptor) => {
    controllerList.push({
      target,
      type,
      path,
      method: name,
      controller: descriptor.value
    })
  }
}

// 接下来我们还需要用来格式化参数的装饰器
export function Parse(type) {
  return (target, name, index) => {
    parseList.push({
      target,
      type,
      method: name,
      index
    })
  }
}

// 以及最后我们要处理的各种参数的获取
export function Param(position) {
  return (key) => (target, name, index) => {
    paramList.push({
      target,
      key,
      position,
      method: name,
      index
    })
  }
}

export const Body   = Param('body')
export const Header = Param('header')
export const Cookie = Param('cookie')
export const Query  = Param('query')
export const Get    = Method('get')
export const Post   = Method('post')

Koa服务的处理

上边是创建了所有需要用到的装饰器,但是也仅仅是把我们所需要的各种信息存了起来,而怎么利用这些装饰器则是下一步需要做的事情了:

const routers = []

// 遍历所有添加了装饰器的Class,并创建对应的Router对象
routerList.forEach(item => {
  let { basename, constrcutor } = item
  let router = new Router({
    prefix: basename
  })

  controllerList
    .filter(i => i.target === constrcutor.prototype)
    .forEach(controller => {
      router[controller.type](controller.path, async (ctx, next) => {
        let args = []
        // 获取当前函数对应的参数获取
        paramList
          .filter( param => param.target === constrcutor.prototype && param.method === controller.method )
          .map(param => {
            let { index, key } = param
            switch (param.position) {
              case 'body':    args[index] = ctx.request.body[key] break
              case 'header':  args[index] = ctx.headers[key]      break
              case 'cookie':  args[index] = ctx.cookies.get(key)  break
              case 'query':   args[index] = ctx.query[key]        break
            }
          })

        // 获取当前函数对应的参数格式化
        parseList
          .filter( parse => parse.target === constrcutor.prototype && parse.method === controller.method )
          .map(parse => {
            let { index } = parse
            switch (parse.type) {
              case 'number':  args[index] = Number(args[index])             break
              case 'string':  args[index] = String(args[index])             break
              case 'boolean': args[index] = String(args[index]) === 'true'  break
            }
          })

        // 调用实际的函数,处理业务逻辑
        let results = controller.controller(...args)

        ctx.body = results
      })
    })

  routers.push(router.routes())
})

const app = new Koa()

app.use(bodyParse())
app.use(compose(routers))

app.listen(12306, () => console.log('server run as http://127.0.0.1:12306'))

上边的代码就已经搭建出来了一个Koa的封装,以及包含了对各种装饰器的处理,接下来就是这些装饰器的实际应用了:

import { Router, Get, Query, Parse } from "../decorators"

@Router('')
export default class {
  @Get('/')
  index (@Parse('number') @Query('id') id: number) {
    return {
      code: 200,
      id,
      type: typeof id
    }
  }

  @Post('/detail')
  detail (
    @Parse('number') @Query('id') id: number, 
    @Parse('number') @Body('age') age: number
  ) {
    return {
      code: 200,
      age: age + 1
    }
  }
}

很轻易的就实现了一个router的创建,路径、method的处理,包括各种参数的获取,类型转换。
将各种非业务逻辑相关的代码统统交由装饰器来做,而函数本身只负责处理自身逻辑即可。
这里有完整的代码:GitHub。安装依赖后npm start即可看到效果。

这样开发带来的好处就是,让代码可读性变得更高,在函数中更专注的做自己应该做的事情。
而且装饰器本身如果名字起的足够好的好,也是在一定程度上可以当作文档注释来看待了(Java中有个类似的玩意儿叫做注解)。

总结

合理利用装饰器可以极大的提高开发效率,对一些非逻辑相关的代码进行封装提炼能够帮助我们快速完成重复性的工作,节省时间。
但是糖再好吃,也不要吃太多,容易坏牙齿的,同样的滥用装饰器也会使代码本身逻辑变得扑朔迷离,如果确定一段代码不会在其他地方用到,或者一个函数的核心逻辑就是这些代码,那么就没有必要将它取出来作为一个装饰器来存在。

参考资料

  1. typescript | decorators
  2. koa示例的原版,简化代码便于举例

One more thing

我司现在大量招人咯,前端、Node方向都有HC
公司名:Blued,坐标帝都朝阳双井
主要技术栈是React,也会有机会玩ReactNative和Electron
Node方向8.x版本+koa 新项目会以TS为主
有兴趣的小伙伴可以联系我详谈:
email: jiashunming@blued.com
wechat: github_jiasm

查看原文

SmallW 收藏了文章 · 2018-03-13

2019开发最快的Webapp框架--BUI交互框架

1.5 版本更新说明

BUI 1.5版本以后变化很大,统一新的风格,新的规范750,新增基于Dom的数据驱动,完善了单页路由页面的生命周期等等,在好用的路上越走越远,如果你也觉得好用,帮我们推荐给您身边的朋友,谢谢。后续我们还会整理一些实战类的教程,欢迎关注BUI Webapp专栏

一、案例代表

点击预览QQ交互效果GIF

这是你看下去的动力, 我用BUI仿照QQ的手机截图做出来的一个demo, 包含QQ的常见交互, 侧滑边栏,TAB切换,侧滑列表,下拉刷新,下拉菜单,弹窗搜索等交互操作, 这几种操作很多UI框架都有, 但几种操作结合在一块, 不同方向之间的交互冲突, 不是那么简单的事情. 使用BUI耗时1天.

通过这个看似简单的例子, 使用BUI快速开发出来的应用是可以上得了台面的.在没有设计稿的情况下, 我们可以仿照任何一款APP开发,并且按照APP的相同比例完美还原. 另外一个是在各种复杂的手势操作交互里面, BUI游刃有余.

请使用手机扫码操作看看.

QQ演示地址

建议扫码在手机预览, PC预览不支持手势操作.

点击PC预览Demo

BUI控件demo

二、前言

今天的主角就是BUI交互框架, 又不仅仅只是框架, 这是一整套快速开发Webapp的解决方案. 请查看BUI官网, 里面有DEMO演示, 给你最直观的感受.

如果你也跟我一样在寻找一款快速开发的Webapp框架, 相信我,你来对地方了. 欢迎加入BUI 移动开发交流群: 691560280
BUI 移动开发交流群2: 4170980

我自己从2011年开始接触移动混合app开发至今, 开发的应用也有几十个了, 这些经验可能对别人也会有所帮助. 于是我把它整理出来, 围绕着快速开发,BUI做了很多事情, 看完你就会明白, BUI的快是有意义的.

面向的开发者

  • 混合App开发者
  • 后端开发者
  • 前端
  • 美工

我对框架的要求

我找过很多框架, 对框架有一些要求:

  1. 简单学习上手快;
  2. 能够轻松定制UI;
  3. 开发一次,跨平台全适配,什么安卓,IOS,微信,淘宝,支付宝,钉钉,浏览器等等;
  4. 控件要多,至少满足平常所需;
  5. 能结合第三方打包平台,打包成独立应用;
  6. 有案例或者模板可以参考;
  7. 支持物理按键后退;
  8. 支持后退刷新;
  9. 支持后退到指定页面带动画;
  10. 模块化开发;
  11. 反应速度快;

...

这样的要求不过分吧?
但发现每个UI框架都有自己的定位, 跟我要的还不太一样, 于是我们只能自己造轮子.
既然自己造轮子,那就要先做最适合自己的,然后才是适合别人. 这里有必要交待一下背景.

三、背景

早在2011年, 品高公司就开始接触移动互联网, 并有了第一个基于Cordova的混合app开发框架及打包平台--Bingotouch (那个时候除了Appcan,没有像Dcloud,APICloud等比较成熟的平台) ,为了配合Bingotouch开发框架, 我们需要有一个UI, 于是我们有了一个简单的UI, 那时只有一些简单的基础控件, 简单的适配(可以在打包自适应,但无法在浏览器预览), 页面切换传参使用原生的切换, 其它复杂点的就用第三方的插件, 下拉刷新就用iscroll, 投入到项目开发中就没去管UI了, 一晃就4年过去了, 简直就是奇迹.

但随着混合应用的技术推广及系统的升级,手机屏幕越来越大,我们的UI存在的一些不足,我们意识到,我们的UI必须升级了. 经过4年的移动开发项目经验沉淀, 于是2015年底我们就开始着手UI的改造.

四、移动开发的难点

这里顺便交待一下我们的旧UI存在的问题吧.

1.viewport问题

旧UI采用viewport固定宽度的方式适应, 把设计稿更改成320大小, 然后我们按这个大小量取间隙,还原设计稿, 这样会导致在浏览器整个页面变大, 之前不考虑浏览器,所以问题还不大, 但随着viewport在不同手机设备的表现, 这种方式瓶颈越来越大, if else 越来越多.

2.UI组件少

早期的时候,基于zepto的控件还要适应移动端, 是少之又少, 所以我们经常需要上网找或者自己写控件, 这样就造成每个控件的使用方式不一, 兼容不一, 较难上手.

3.不同平台需要多次开发

由于页面切换是使用原生切换,这样体验较好, 但是也带来一个问题,跟框架结合过于紧密, 有些项目需求, 开发以后,客户要求在微信上线, 这样就得开发2次, 如果还要在其它平台呢? 不敢想象.

...

正是因为有了这些问题, 才让我们的新BUI适应性越来越强.

五、设计思路

很多看完文档不太理解BUI的更多是Native模块, 这里是我们区别于其它webapp开发框架的地方, BUI的一个控件或者一个方法不止做一件事情. 我们这里主要分析一下Native模块的设计.

前面谈到我们的页面切换传参是用的原生切换, 在安卓2.多的系统,如果使用location.href跳转会泛白,单页在不同安卓下的表现也不尽完美, 所以我们这里采用的原生, 而原生在web浏览器是不能运行的. 于是我们想采用一种兼容的方式, 像混合应用一样, 中间做一层转换, 这样就可以各取所需, 需要web时, 整体切换成web.

Method <- web <- BUI -> Bingotouch -> Native

1. 思路1

BUI设计思路1

思路解析: bui 通过 bui.isWebapp (旧版是bui.debug) 来选择是web,或者native, 所有native模块需要在 bui.ready 里面去初始化, 就这样,我们把Bingotouch 里面常用的交互, 页面跳转, 传参, 刷新, 回退, 请求, 上传, 下载 等常用的功能模块做了封装统一API, 每个native模块的方法都对应两个不同的方法, 这样在使用过程,用户不需要关注是原生还是web, 只需要知道,我在浏览器运行, 就设置 bui.isWebapp 为true, 需要打包, 就用 bui.isWebapp 为false;

BUI设计思路2

以上就是我们的BUI 为我们公司的Bingotouch而实现的一套UI思路, 细心的你可能会发现, Bingotouch 跟互联网上的第三方平台很像, 没错的, 但是我们出来比较早. 这点我觉得很引以为豪. 所以看下那个设计思路, 我们也是为了扩展到第三方平台预留了接口. 于是我们有了不同版本的 bui.js .

2. 思路2

BUI设计思路3

场景1: 客户指定要使用第三方平台打包;

我们的Bingotouch就派不上用场,即使我们很优秀, 但是客户不放心, 用第三方平台后面可以不依赖于某一家公司, 可以很容易找到其他开发者, 客户的担心也不无道理.

第三方平台的出现方便了开发者,但也带来了新的难题, 一个第三方平台,会有自己的开发生态, 工具, API, UI, 开发者要学习的东西太多.

于是我们BUI又可以派上用场了, 我们还是用我们熟悉的方式开发, 只是特殊部分原生接口,使用第三方的就可以了, 这样你只需要学习一次BUI,就能开发其它平台,使用其它平台的部分原生接口,想想是很美好的事情. 后面发现每个平台的上传,下载,文件处理等的方式都不同, 处理起来很耗费时间, 针对第三方平台我们精简了部分Native方法, 只保留了 页面加载,传参,后退,请求,刷新这些.

这样, 客户不管指定哪个平台,都可以使用我们的UI来做开发交互, 开发人员都不用再学习新的UI, 能减少一点是一点啊.

六、框架特点

围绕这个想法去设计, 再分析了互联网上的一些UI的特点, 我们的框架应该是这样子的:
介于webapp跟混合app开发之间, 大部分应用有80%以上的功能都是由UI实现的, 这样通过学习一次BUI, 你就可以游刃于不同平台之间.

BUI成本优势

1. 简单学习,快速上手

在软件公司里面, 开发者占多数以及人员的流动等因素, 如果开发者愿意去学习使用, 可以解决招不到合适的前端问题. 所以学习要快,简单.

技术选型

我们在技术上没有依赖于vue,react等先进理念, 我们就是要简单, Dom是最基础也是最容易理解的, 所以我们基于zepto或者jquery.

工具

开发者习惯用什么工具,就用什么工具.习惯用什么 webpack,gulp,sass,less 完全由自己做主. 但是如果使用我们推荐的sublimetext工具, 我们有针对这个工具的一个插件, 可以快速书写,事半功倍.

丰富的文档及入门视频

BUI的入门文档只需要5分钟
BUI的控件API文档
BUI的规范文档
BUI的FAQ

2. 多终端多平台适配

我们采用独有的规范,使用REM适配,开发出来的页面,能够在各种安卓IOS系统,webkit浏览器(淘宝,微信,QQ,钉钉),第三方打包平台(Dcloud,APICloud,APPCan等) 不同分辨率,保持一致的缩放效果. 跟web保持一致的切图习惯,只是在做单位转换的时候, 是基于540px设计稿,1rem=100px;
这是很多UI欠缺的, 给你一个设计稿,没有什么指引告诉你图要怎么切, 大部分是采用响应式自适应出来的效果, 看上去一样,实际效果是跟效果图有出入的.

如图, 其它UI的适配的高度是固定的, 随着屏幕增高, 底部的空白会越来越多, 而BUI是整体等比缩放, 像一张大图缩放到合适的屏幕一样. 真正是一次开发,多平台适配.

其它UI的适配:

其它UI的适配

BUI的适配:

BUI的适配

3. 丰富的交互组件

BUI控件演示Demo

前面我们也说过,旧UI我们需要经常找插件,找来的插件也不一定能用, 调用系统原生的交互,在安卓跟ios会有不同的表现, 为了避免这种情况, 我们把互联网上常见的插件都统一开发出来, 一致的交互, 一致的使用方式, 一致的API.

BUI的组件还有些不太一样的地方, 我们关注控件的交互,主张相同交互由同一个控件实现. 我们的组件是内容跟结构交互分离, 一个组件可以做多件事情, 例如:

bui.slide 是一个焦点图组件, 它除了自身支持, 横向,纵向,全屏,自动播放等功能以外, 我们稍微改变了参数, 它就变成了 tab 组件.

**(这里不能直接上传gif交互图,我把重要的交互展示一下,想看更多交互请直接点击预览交互效果)**

bui.slide 焦点图控件

预览交互效果

js:
var uiSlide = bui.slide({
    id:"#uiSlide",
    height:200
})
html:
<div id="uiSlide" class="bui-slide">
    <div class="bui-slide-main">
        <ul>
            <li>
                <!--第1屏-->
                <img data-original="" alt="">
                <div class="bui-slide-title">图片标题</div>
            </li>
        </ul>
    </div>
    <!-- 分屏菜单 -->
    <div class="bui-slide-head">
        <ul >
            <li>1</li>
        </ul>
    </div>
</div>

TAB 选项卡

TAB初始化

js: 
var uiSlideTab = bui.slide({
    id:"#uiSlideTab",
    menu:".bui-nav",
    children:".bui-tab-main ul",
    scroll: true
})
html:
<div id="uiSlide" class="bui-tab">
    <div class="bui-tab-head">
        <ul class="bui-nav">
            <!-- 分屏菜单 active 是激活的TAB样式 -->
            <li class="bui-btn active">Tab1</li>
        </ul>
    </div>
    <div class="bui-tab-main">
        <ul>
            <li>
                第1屏
            </li>
        </ul>
    </div>
</div>

仔细观察下这两个的结构及交互,变的只是父层的样式, 其它结构及操作都是一样的. 我们把TAB的结构再拆分一下, 就变成了 TAB在顶部以及在底部的效果.

TAB在顶部
TAB在顶部

TAB在底部
TAB在底部

TAB在侧边
TAB在侧边

这样是方便了,但是要记住的参数很多,怎么破? 这就要谈到我们的BUI Fast插件了, 一个快速书写的 Sublimetext插件, 这个后面篇幅再介绍. 我们把比较重要的组件给大家介绍一下.

还有什么是bui.slide能做的? 例如: 微信里面的推广,经常是整屏往上滑动, 我还用过 bui.slide 去拓展了一个简单的有动画效果的路由. 发挥你的想象前, 先仔细了解控件的特性. 后面我们有自己更简单的单页路由.

bui.list 下拉刷新及上拉加载控件

bui.list是基于 bui.scroll 扩展的一个加载分页及刷新的一个控件, 默认把分页都处理好了,你只需要配置一个数据请求的地址, 能返回一个数组就行了, 如果返回的字段不一样, 也是提供了字段映射的配置, 开发一个加载及下拉刷新功能的列表, 只要5分钟. 如果这个满足不了你的需求, 你可以再看 bui.scroll 及 bui.pullrefresh 这两个控件.

bui.list

bui.dialog 弹出层组件

bui.dialog 弹出层插件同样是一个扩展性特别强的插件, 支持从不同方向进入, 支持关闭打开, 支持全屏等, 支持动态创建以及静态渲染,
这两者的区别 例如,

动态渲染
bui.confirm, bui.alert, bui.prompt 都是基于dialog动态创建的控件, 这种方式简单,只需要传入content参数;
静态渲染
还有bui.select,bui.actionsheet, bui.pickerdate 等控件. select及actionsheet 的底层都是一个静态渲染的对话框, 这样有个好处就是, 对话框的内容都自由定义.

预览交互效果GIF

bui.select 选择控件

选择控件,支持单选,多选, 支持弹窗或者不弹窗, 支持静态或者动态渲染.

图片描述

bui.actionsheet 上拉菜单控件

上拉菜单控件, 常用于分享

图片描述

bui.swipe 滑动组件

bui.swipe 也是很强大的一个控件, 解决同一个页面的各种手势操作冲突. 像 bui.sidebar ,bui.listview 都是基于bui.swipe扩展的插件.

图片描述

bui.sidebar 侧边栏组件

支持左边跟右边,支持同时, 但还是建议同一个页面不要出现太多这类交互.

图片描述

bui.listview 侧滑列表组件

侧滑列表控件,支持菜单在左边或者右边

图片描述

bui.hint 自动消失提醒组件

消息提醒自动消失, 支持上中下等方向, 并且支持自动关闭或者手动关闭, 可以指定在某个容器内.

图片描述

bui.accordion 折叠组件

折叠菜单, 显示隐藏, 支持全部显示,或者一次只能显示一个.

图片描述

bui.dropdown 下拉菜单组件

下拉菜单, 常用于搜索旁边的选择, 选项不宜过多.

图片描述
这些都是基本的组件, BUI官网其实还有很多, 就不一一列举了.

4. 轻松定制UI

BUI是有规范可循的,这个在输出设计稿的时候,尤其有用. 而且BUI更多的是关注UI交互这块, 只要遵循BUI的结构规范, 里面的内容是可以自由定制的, UI也是随你怎么修改,只要ID不变, 交互就不变.

5. 快速开发

重头戏来了, 前面这些都只是铺垫, 只有简单的文档, 丰富的组件, 这些每个框架都有, 不能算快.
我们围绕着开发人员快速开发, 做了很多事情, 让你如虎添翼.

5.1 BUIFast插件

BUIFast 是Sublimetext的一个插件, 前面我们也说了,我们并不要求用户使用某个工具, 但如果用户使用我们推荐的工具, 那就是如虎添翼. Sublimetext是一个优秀的编辑器. 安装好BUIFast插件以后, 你可以使用BUI的快速书写.

规则如下:
ui- 生成结构, bui- 生成脚本

ui-html 生成BUI的标准引用
ui-page 生成BUI标准结构页面

ui-控件名 生成 控件的结构;
bui-控件名 生成 控件的初始化;
两个前呼后应.

后面我们新增了 bu-控件名-demo 直接生成示例代码. 所以前面说的, 你根本不需要去记住结构,你只需要记住有什么控件名. 比方 bui-slide-tab

点击预览效果22

5.2 buijs Node命令行工具

全局安装buijs以后,每次创建如果有新版本都会从github获取, 可以在任意地方创建, 并且可以指定模板及打包平台.

安装

sudo npm install -g buijs

简单创建工程包(默认webapp平台)

buijs create demo

指定模板
同一个工程可以重复新增模板, main- 开头会覆盖main 模块, page-开头是新增模块. 点击查看更多模板预览 部分模板我们还增加了常用的交互处理, 比如登录输入框的删除, 注册发送验证码的倒计时, 如果不需要可以自行删除.

buijs create demo -t main-tab

指定dcloud平台
指定平台以后,创建的工程可以直接覆盖到平台新创建的应用目录, 默认bui绑定了物理后退按键的处理.

buijs create demo -p dcloud

点击预览效果23

5.3 模板库

模板库有2个,一个是配合buijs工具创建的, 但由于每次创建都会获取最新,如果太多模板,这里创建工程就会偏慢, 所以我们只把常用的模板放github上面, 我们还有一个新模板库, 那里下载下来每个都是 CSS,js,html 一起的.

模板库

5.4 插件库

bui的官方组件只是一些基本的组件, 后续还会扩展.

5.5 案例库

除了插件,模板,官网还提供了一个案例库, 这个案例是一些通用的例子, 下载下来就能拿去开发.
这里比较有代表性的一个案例是仿照QQ的一个交互, 交互复杂,但开发起来我们只耗时1天.

BUI代表案例QQ

点击预览
建议扫码在手机预览, PC预览不支持手势操作.

七、设计思路改进

1. 去平台化

从BUI出来诞生到现在, 在品高内部已经两年了, 服务了近百个项目, 其中也暴露出来了一些问题.

1.第三方打包平台如雨后春笋般增长, 我们的兼容何时才休?

用户在做开发的时候,一开始选择了平台其实也不会再转换其它平台了, 虽然我们提供了这种可能, 对于用户来说, 在webapp及打包平台间切换用场会比 APICloud 平台转换到 Dcloud平台之类的场景要多得多, 所以我们目前只兼容 Bingotouch, APICloud, Dcloud 有对应的 bui.js , 其它平台暂时不考虑.

2.由于我们基于DOM开发,入门门槛低,带来的问题是开发过程中的代码质量无法保证;
3.每个人喜欢用的模块化不一样, 有人喜欢用requirejs, 有人喜欢用seajs, 同样的模块不能在项目之间共享;

2. 单页路由

在用第三方平台的原生跳转里面, 也有一定的局限性, 比方, 后退刷新, 后退多层到指定页面 等, 这些每个平台的处理都不一样, 而且有的都不允许这么处理. 我们需要更灵活的路由, 支持效果自定义, 支持后退刷新, 支持后退到指定页面, 支持物理后退 等, 这一切创建对应工程包的时候, 你就已经拥有.

路由效果

这里我们默认采用的是微信的切换效果, 你还可以选择其它交互, 例如 dcloud 的 cover, QQ 的slide 切换效果, 这些效果都是为了让你的应用更好的嵌入对应的平台. 这种特别在原生跟轻应用之间切换较为常用.

预览交互效果26

3. 模块化

我们使用我们自己的模块化,跟路由一起配合使用, 开发的模块在项目之间的共享成为了可能, 我们还按照requirejs,seajs 的接口设置, 可以兼容之前的模块.

4. 路由及模块化的原理

当你打开index.html的时候,就会自动加载main模块, 模块的命名, 除了main, 其它匿名模块默认都是.html前面的路径名, 比方: main 模块有个按钮, 按钮有个链接(注意不能使用a标签)

main模块, 路径: pages/main/main.html

<div class="bui-btn" href="pages/sidebar/sidebar.html">跳转到sidebar</div>

点击会自动跳转到 pages/sidebar/sidebar.html , 这时会自动加载 pages/sidebar/sidebar.js

sidebar.js 路径: pages/sidebar/sidebar.js

loader.define(function(require,exports,module){
    
    module.exports = {};
})

这样, 如果其它页面要调用sidebar的模块

loader.require("pages/sidebar/sidebar",function(sidebar){
   //  
})

这是路由最简单的用法, 当然他还有很多其它定义, 具体需要自己查看API了.

路由

八、总结

与其说bui是一个交互框架,我觉得我们更像是一个移动快速开发解决方案, 围绕开发过程中的效率,一点一点的进步优化, 我们也针对了常用的平台的快速创建工程包, 创建完覆盖到对应的平台应用包里面就能使用了, 是不是开发最快的webapp框架? 我希望是, 但我更希望整理的这些内容对别人有所帮助.

BUI的目的不是要成为一个很优秀的框架, 而是可以帮助大家解决80%问题的框架.

一个人快, 节省的只是一个项目的时间, 如果每个人都能开发更快, 节省的是N个项目的时间. 后面我们会在 segmentfault的 BUI的专栏 里,把一些常用的技巧,及使用上的FAQ整理一下, 欢迎一起理性探讨.

查看原文

SmallW 收藏了文章 · 2018-03-13

使用h5新特性,轻松监听任何App自带返回键(最新版)

1、前言

如今h5新特性、新标签、新规范等有很多,而且正在不断完善中,各大浏览器商对它们的支持,也是相当给力。作为前端程序员,我觉得我们还是有必要积极关注并勇敢地加以实践。接下来我将和各位分享一个特别好用的h5新特性(目前也不是特别新),轻松监听任何App自带的返回键,包括安卓机里的物理返回键,从而实现项目开发中进一步的需求。

2、起因

大概半年前接到pm这需求,用纯h5实现多audio的播放、暂停、续播,页面放至驾考宝典App中,与客户端没有任何的交互,换言之就是混合型开发,功能型h5。所以与客户端相关的js不需要引用。那为什么不让客户端实现让前端h5实现?更何况App里的返回键还是原生的,ios和安卓里的双方协议可能还不一样。这是我撸码之前提出的疑问。pm小姐姐回我客户端还得发版、审核,太麻烦了,h5一个线上链接就搞定了。好吧,既然是这样,看上去这需求也挺简单的嘛,虽然之前也没做过类似的需求。不管三七二十一,撸起袖子就是干,开始了填坑之旅。

3、具体页面展示

clipboard.png

clipboard.png

这就是设计稿及最终成稿图了。用户点击任何一个图标都可以实现音频的播放,而且点击的同时图标会发生变化,再点击相同图标即可实现暂停,以此类推。当第一个音频正在播放时,点击其他图标,第一个音频会自动暂停,以此类推。我相信做过类似需求的朋友,应该会很熟悉产品到底希望我们前端做成什么样。所以,这里会有几个坑,不仅仅是监听App的返回键。我先说第一个。

第一: 播放音频时,需要秒开,而不是等待一秒甚至更长时间。由于产品最开始给我的是本地音频文件,当时我也没考虑到线上音频文件(这样加载也许会更快但也许也会更慢),因为audio默认加载模式是先下载完,当音频就绪后再播放。所以最后我采取预加载模式,提高加载效率,尽可能让用户感受不到有延迟的存在。代码如下:

clipboard.png

这样即可。但这只规避了第一个坑。第二个坑或可以称之为自主优化,即播放进度条的时刻显示和暂停了。需求里是没有播放进度条的,但在需求上线后我和pm小姐姐沟通的过程中,对方肯定、称赞了我的设计。播放进度条的设计和增加,更接近原生App、用户体验也更好。以及页面初始化时,菊花图的存在,也很有意义。自定义播放进度条,根据当前播放的音频时长而变化,该暂停就暂停,该重新初始化就重新初始化,很好的融合进页面。希望这点也帮助到大家。而涉及的代码,具体会用到canplay、timeupdate事件,以及load()函数等。此处我个人用jq实现的。

clipboard.png

第二、DOM结构的渲染。非常不推荐在页面里写死,六个较少,勉强可以这么写。但是一旦数量增加,非常不可取。我最开始用的是传统的字符串拼接实现,但后来我导师建议我用模板引擎渲染,而驾考宝典事业部默认用artTemplate.js,最后我便修改了下。它分为原生JS和jq两个版本写法,各位也可自行研究下————模板引擎和拼接字符串的优缺点。如果自行设计一个模板引擎,该如何设计。

4、接下来着重介绍下我具体是怎么监听任何App自带的返回键,以及安卓机里的物理返回键。

那为什么我要去监听呢,这里我有必要强调强调再强调。苹果手机不管是微信、QQ、App,还是浏览器里,涉及到audio、video,返回上一页系统会自动暂停当前的播放的,但不是所有安卓机都可以。所以我们自己必须自定义监听。很多朋友可能第一想法就是百度,然后出来的答案无非是这样

pushHistory(); 
window.addEventListener("popstate", function(e) { 
    alert("我监听到了浏览器的返回按钮事件啦");//根据自己的需求实现自己的功能 
}, false); 
function pushHistory() { 
    var state = { 
        title: "title", 
        url: "#"
    }; 
    window.history.pushState(state, "title", "#"); 
}

是不是很眼熟?然而关键需求不能完美实现,要这段代码有何用,当时我也是绞尽脑汁。直到经过大神好友指导,复制了这段代码

var hiddenProperty = 'hidden' in document ? 'hidden' :    
    'webkitHidden' in document ? 'webkitHidden' :    
    'mozHidden' in document ? 'mozHidden' :    
    null;
var visibilityChangeEvent = hiddenProperty.replace(/hidden/i, 'visibilitychange');
var onVisibilityChange = function(){
    if (document[hiddenProperty]) {    
        console.log('页面非激活');
    }else{
        console.log('页面激活')
    }
}
document.addEventListener(visibilityChangeEvent, onVisibilityChange);

所有问题迎刃而解。
这段代码的原理我个人理解就是通过判断用户浏览的是否为当前页,从而进行相关操作。
这是 MDN相关链接: https://developer.mozilla.org...

所以最后我是这么处理的,代码截图如下:

clipboard.png

5、手机兼容性

众所周知现在的安卓机系统4.0等都是低配版了,该属性大部分安卓机都能识别,个人低配版安卓机无法识别,原因在于navigator.userAgent内核版本过低,chrome现在很多是64+了,所以遇到该问题只要想办法兼容它就好了。

综上,遇到这个问题各位也不要过度担心,办法肯定是有的,但不是一根筋,而是通过转变思路,快速实现需求。希望这个特性能帮到大家。

6、感谢及呼吁

自从18年3月我公开发表了这篇文章,当时写的很简洁,但阅读量还是有不少,我挺吃惊的,谢谢各位的认可。但随后包括直到今天,我发现有不少知名微信公众号和网站都转载了我的这篇文章,有些并没有标明原文地址和作者,这让我也很意外和失望。所以最后希望大家能互相尊重吧,最终每个人的劳动成果,谢谢。在转载文章时,标注真实的原文地址和作者。这也是为什么我这次要更新文章的原因。另外,现在我已经不做常规的业务开发,本人已转投h5游戏开发,集游戏创意、美术、编码、测试于一身,欢迎同道朋友交流哦。

查看原文

SmallW 收藏了文章 · 2018-02-28

面试的信心来源于过硬的基础

在过去的一年很多人不满于公司没有福利、人际关系不好相处、没有发展前途的境遇等等,想着在开年来换一份工作来重新开始自己,那么 你 准备好了吗?

下面是本人整理的一份面试材料,本想自己用的,但是新年第一天 公司突然给了我个惊喜,涨工资了!!!

1、 viewport

    <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
    // width    设置viewport宽度,为一个正整数,或字符串‘device-width’
    // device-width  设备宽度
    // height   设置viewport高度,一般设置了宽度,会自动解析出高度,可以不用设置
    // initial-scale    默认缩放比例(初始缩放比例),为一个数字,可以带小数
    // minimum-scale    允许用户最小缩放比例,为一个数字,可以带小数
    // maximum-scale    允许用户最大缩放比例,为一个数字,可以带小数
    // user-scalable    是否允许手动缩放

延伸 提问

怎样处理 移动端 1px 被 渲染成 2px 问题


    1 局部处理
        meta标签中的 viewport属性 ,initial-scale 设置为 1 
        rem 按照设计稿标准走,外加利用transfrome 的scale(0.5) 缩小一倍即可;
    2 全局处理
        meta标签中的 viewport属性 ,initial-scale 设置为 0.5
        rem 按照设计稿标准走即可

2、跨域的几种方式

首先了解下浏览器的同源策略
同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

那么怎样解决跨域问题的呢?

    1 通过jsonp跨域
        1.)原生实现:
         <script>
            var script = document.createElement('script');
            script.type = 'text/javascript';
        
            // 传参并指定回调执行函数为onBack
            script.src = 'http://www.....:8080/login?user=admin&callback=onBack';
            document.head.appendChild(script);
        
            // 回调执行函数
            function onBack(res) {
                alert(JSON.stringify(res));
            }
         </script>
    2、 document.domain + iframe跨域  
        此方案仅限主域相同,子域不同的跨域应用场景。
        1.)父窗口:(http://www.domain.com/a.html)

            <iframe id="iframe" data-original="http://child.domain.com/b.html"></iframe>
            <script>
                document.domain = 'domain.com';
                var user = 'admin';
            </script>
            2.)子窗口:(http://child.domain.com/b.html)
            
            <script>
                document.domain = 'domain.com';
                // 获取父窗口中变量
                alert('get js data from parent ---> ' + window.parent.user);
            </script>

        弊端:请看下面渲染加载优化

    3、 nginx代理跨域
    4、 nodejs中间件代理跨域
    5、 后端在头部信息里面设置安全域名
    
    更多跨域的具体内容请看  https://segmentfault.com/a/1190000011145364
    

3、 渲染优化

    1.禁止使用iframe(阻塞父文档onload事件);
        *iframe会阻塞主页面的Onload事件;
        *搜索引擎的检索程序无法解读这种页面,不利于SEO;
        *iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。

        使用iframe之前需要考虑这两个缺点。如果需要使用iframe,最好是通过javascript
        动态给iframe添加src属性值,这样可以绕开以上两个问题。

    2.禁止使用gif图片实现loading效果(降低CPU消耗,提升渲染性能);
    3、使用CSS3代码代替JS动画(尽可能避免重绘重排以及回流);
    4、对于一些小图标,可以使用base64位编码,以减少网络请求。但不建议大图使用,比较耗费CPU;
            小图标优势在于:
                1.减少HTTP请求;
                2.避免文件跨域;
                3.修改及时生效;

    5、页面头部的<style></style> 会阻塞页面;(因为 Renderer进程中 JS线程和渲染线程是互斥的);
    6、页面头部<script</script> 会阻塞页面;(因为 Renderer进程中 JS线程和渲染线程是互斥的);
    7、页面中空的 href 和 src 会阻塞页面其他资源的加载 (阻塞下载进程);
    
    8、网页Gzip,CDN托管,data缓存 ,图片服务器;
    9、前端模板 JS+数据,减少由于HTML标签导致的带宽浪费,前端用变量保存AJAX请求结果,每次操作本地变量,不用请求,减少请求次数
    10、用innerHTML代替DOM操作,减少DOM操作次数,优化javascript性能。
    11、当需要设置的样式很多时设置className而不是直接操作style。
    12、少用全局变量、缓存DOM节点查找的结果。减少IO读取操作。
    13、避免使用CSS Expression(css表达式)又称Dynamic properties(动态属性)。
    14、图片预加载,将样式表放在顶部,将脚本放在底部  加上时间戳。

    15、 避免在页面的主体布局中使用table,table要等其中的内容完全下载之后才会显示出来,显示比div+css布局慢。
        对普通的网站有一个统一的思路,就是尽量向前端优化、减少数据库操作、减少磁盘IO。
            向前端优化指的是,在不影响功能和体验的情况下,能在浏览器执行的不要在服务端执行,
            能在缓存服务器上直接返回的不要到应用服务器,程序能直接取得的结果不要到外部取得,
            本机内能取得的数据不要到远程取,内存能取到的不要到磁盘取,缓存中有的不要去数据库查询。
            减少数据库操作指减少更新次数、缓存结果减少查询次数、将数据库执行的操作尽可能的让你的程序完成(例如join查询),
            减少磁盘IO指尽量不使用文件系统作为缓存、减少读写文件次数等。程序优化永远要优化慢的部分,换语言是无法“优化”的。
            

4、事件的各个阶段

1:捕获阶段 ---> 2:目标阶段 ---> 3:冒泡阶段
document   ---> target目标 ----> document

由此,addEventListener的第三个参数设置为true和false的区别已经非常清晰了:

true表示该元素在事件的“捕获阶段”(由外往内传递时)响应事件;

false表示该元素在事件的“冒泡阶段”(由内向外传递时)响应事件。

5、let var const

let 允许你声明一个作用域被限制在块级中的变量、语句或者表达式
    let绑定不受变量提升的约束,这意味着let声明不会被提升到当前
    该变量处于从块开始到初始化处理的“暂存死区”。

var 声明变量的作用域限制在其声明位置的上下文中,而非声明变量总是全局的
    由于变量声明(以及其他声明)总是在任意代码执行之前处理的,所以在代码中的任意位置声明变量总是等效于在代码开头声明
    
const 声明创建一个值的只读引用 (即指针)
    这里就要介绍下 JS 常用类型 
    String、Number、Boolean、Array、Object、Null、Undefined
    其中基本类型 有 Undefined、Null、Boolean、Number、String,保存在栈中;
    复合类型 有 Array、Object ,保存在堆中;
    
    基本数据当值发生改变时,那么其对应的指针也将发生改变,故造成 const申明基本数据类型时,
    再将其值改变时,将会造成报错, 例如 const a = 3 ; a = 5 时 将会报错;
    但是如果是复合类型时,如果只改变复合类型的其中某个Value项时, 将还是正常使用;

6、箭头函数

    语法比函数表达式更短,并且不绑定自己的this,arguments,super或 new.target。这些函数表达式最适合用于非方法函数,并且它们不能用作构造函数。
    

7、快速的让一个数组乱序

    var arr = [1,2,3,4,5,6,7,8,9,10];
    arr.sort(function(){
        return Math.random() - 0.5;
    })
    console.log(arr);

此处解释:(语言组织能力不足,请勿吐槽)

首先: 当return 的值

    小于 0 ,那么 a 会被排列到 b 之前;
    等于 0 , a 和 b 的相对位置不变;
    大于 0 , b 会被排列到 a 之前;

这里你会 发现起始 的时候数组是正序排列,每当进行一次排列的时候, 都会先随机一个随机数 
(注意这里的每一次排列 指 每一个红框指一次排列, 共9次排列 , 一次排列中可能存在多次比较);

当一次排列的 随机数大于0.5 时 将会进行第二次比较, 当第二次随机数 仍然大于0.5 时 ,
    将会再 进行一次比较, 直到 随机数大于0.5 或者排列到第一位;

当一次排列的 随机数 小于0.5时 当前比较的两项 索引将不会改变 ,继续下一次 的排列;

8、字体font-family

    @ 宋体      SimSun
    @ 黑体      SimHei
    @ 微信雅黑   Microsoft Yahei
    @ 微软正黑体 Microsoft JhengHei
    @ 新宋体    NSimSun
    @ 新细明体  MingLiU
    @ 细明体    MingLiU
    @ 标楷体    DFKai-SB
    @ 仿宋     FangSong
    @ 楷体     KaiTi
    @ 仿宋_GB2312  FangSong_GB2312
    @ 楷体_GB2312  KaiTi_GB2312  
    @
    @ 说明:中文字体多数使用宋体、雅黑,英文用Helvetica
    
    body { font-family: Microsoft Yahei,SimSun,Helvetica; } 

9、可能用到的meta标签

    
    <!-- 设置缩放 -->
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, minimal-ui" />
    <!-- 可隐藏地址栏,仅针对IOS的Safari(注:IOS7.0版本以后,safari上已看不到效果) -->
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <!-- 仅针对IOS的Safari顶端状态条的样式(可选default/black/black-translucent ) -->
    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
    <!-- IOS中禁用将数字识别为电话号码/忽略Android平台中对邮箱地址的识别 -->
    <meta name="format-detection"content="telephone=no, email=no" />

    其他meta标签
    <!-- 启用360浏览器的极速模式(webkit) -->
    <meta name="renderer" content="webkit">
    <!-- 避免IE使用兼容模式 -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <!-- 针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,比如黑莓 -->
    <meta name="HandheldFriendly" content="true">
    <!-- 微软的老式浏览器 -->
    <meta name="MobileOptimized" content="320">
    <!-- uc强制竖屏 -->
    <meta name="screen-orientation" content="portrait">
    <!-- QQ强制竖屏 -->
    <meta name="x5-orientation" content="portrait">
    <!-- UC强制全屏 -->
    <meta name="full-screen" content="yes">
    <!-- QQ强制全屏 -->
    <meta name="x5-fullscreen" content="true">
    <!-- UC应用模式 -->
    <meta name="browsermode" content="application">
    <!-- QQ应用模式 -->
    <meta name="x5-page-mode" content="app">
    <!-- windows phone 点击无高光 -->
    <meta name="msapplication-tap-highlight" content="no">

10、消除transition闪屏

    .css {
        -webkit-transform-style: preserve-3d;
        -webkit-backface-visibility: hidden;
        -webkit-perspective: 1000;
    }
    过渡动画(在没有启动硬件加速的情况下)会出现抖动的现象, 以上的 解决方案只是改变 视角 来启动硬件加速的一种方式;
    启动硬件加速的 另外一种方式: 
        .css {
            -webkit-transform: translate3d(0,0,0);
            -moz-transform: translate3d(0,0,0);
            -ms-transform: translate3d(0,0,0);
            transform: translate3d(0,0,0);
        }
    
    启动硬件加速
    最常用的方式:translate3d、translateZ、transform

    opacity属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)

    will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,
    其它属性并不会变成复合层),

    弊端: 硬件加速会导致 CPU性能占用量过大,电池电量消耗加大 ;因此 尽量避免泛滥使用硬件加速。

11、android 4.x bug

    1.三星 Galaxy S4中自带浏览器不支持border-radius缩写
    2.同时设置border-radius和背景色的时候,背景色会溢出到圆角以外部分
    3.部分手机(如三星),a链接支持鼠标:visited事件,也就是说链接访问后文字变为紫色
    4.android无法同时播放多音频audio
    5.oppo 的border-radius 会失效

12、JS 判断设备来源

    function deviceType(){
        var ua = navigator.userAgent;
        var agent = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];    
        for(var i=0; i<len,len = agent.length; i++){
            if(ua.indexOf(agent[i])>0){         
                break;
            }
        }
    }
    deviceType();
    window.addEventListener('resize', function(){
        deviceType();
    })


    微信的 有些不太一样
    function isWeixin(){
        var ua = navigator.userAgent.toLowerCase();
        if(ua.match(/MicroMessenger/i)=='micromessenger'){
            return true;
        }else{
            return false;
        }
    }

13、audio元素和video元素在ios和andriod中无法自动播放

    
    原因: 因为各大浏览器都为了节省流量,做出了优化,在用户没有行为动作时(交互)不予许自动播放;

    /音频,写法一
    <audio data-original="music/bg.mp3" autoplay loop controls>你的浏览器还不支持哦</audio>
    
    //音频,写法二
    <audio controls="controls"> 
        <source data-original="music/bg.ogg" type="audio/ogg"></source>
        <source data-original="music/bg.mp3" type="audio/mpeg"></source>
        优先播放音乐bg.ogg,不支持在播放bg.mp3
    </audio>
    
    //JS绑定自动播放(操作window时,播放音乐)
    $(window).one('touchstart', function(){
        music.play();
    })
    
    //微信下兼容处理
    document.addEventListener("WeixinJSBridgeReady", function () {
        music.play();
    }, false);
    
    //小结
    //1.audio元素的autoplay属性在IOS及Android上无法使用,在PC端正常;
    //2.audio元素没有设置controls时,在IOS及Android会占据空间大小,而在PC端Chrome是不会占据任何空间;
    //3.注意不要遗漏微信的兼容处理需要引用微信JS;

14、css实现单行文本溢出显示 ...

直接上效果:相对于多行文本溢出做处理, 单行要简单多,且更容易理解。

实现方法

overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
当然还需要加宽度width属来兼容部分浏览。

15、实现多行文本溢出显示...

效果:

实现方法:

display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;

适用范围:

因使用了WebKit的CSS扩展属性,该方法适用于WebKit浏览器及移动端;

注:

1、-webkit-line-clamp用来限制在一个块元素显示的文本的行数。 为了实现该效果,它需要组合其他的WebKit属性。常见结合属性:
2、display: -webkit-box; 必须结合的属性 ,将对象作为弹性伸缩盒子模型显示 。
3、-webkit-box-orient 必须结合的属性 ,设置或检索伸缩盒对象的子元素的排列方式 。

如果你觉着这样还不够美观, 那么就接着往下看:

效果:

这样 是不是你想要的呢?

实现方法:

div {
    position: relative;
    line-height: 20px;
    max-height: 40px;
    overflow: hidden;
}

div:after {
    content: "..."; position: absolute; bottom: 0; right: 0; padding-left: 40px;
    background: -webkit-linear-gradient(left, transparent, #fff 55%);
    background: -o-linear-gradient(right, transparent, #fff 55%);
    background: -moz-linear-gradient(right, transparent, #fff 55%);
    background: linear-gradient(to right, transparent, #fff 55%);
}

不要只顾着吃,要注意胃口,此方法有个弊端 那就是 【未超出行的情况下也会出现省略号】 ,这样会不会很挫!!! 没办法,只能结合JS 进行优化该方法了。

注:


1、将height设置为line-height的整数倍,防止超出的文字露出。
2、给p::after添加渐变背景可避免文字只显示一半。
3、由于ie6-7不显示content内容,所以要添加标签兼容ie6-7(如:<span>…<span/>);兼容ie8需要将::after替换成:after。

16、让图文不可复制

这点应该大家 都很熟悉了, 某些时候【你懂的】为了快捷搜索答案,可是打死也不让你复制

-webkit-user-select: none; 
-ms-user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
user-select: none;

那有些网页为了尊重原创,复制的文本 都会被加上一段来源说明,是如何做到的呢?问的好! 等的就是你这个问题 -_- 。

大致思路:


1、答案区域监听copy事件,并阻止这个事件的默认行为。
2、获取选中的内容(window.getSelection())加上版权信息,然后设置到剪切板(clipboarddata.setData())。

17、盒子垂直水平居中

这个问题好像面试必问的吔!反正我是必问的,哈哈!!! 其实无关多少种实现思路,只要你能实现就可以!

提供4种方法

1、定位 盒子宽高已知, position: absolute; left: 50%; top: 50%; margin-left:-自身一半宽度; margin-top: -自身一半高度;

2、table-cell布局 父级 display: table-cell; vertical-align: middle;  子级 margin: 0 auto;

3、定位 + transform ; 适用于 子盒子 宽高不定时; (这里是本人常用方法)
    
    position: relative / absolute;
    /*top和left偏移各为50%*/
       top: 50%;
       left: 50%;
    /*translate(-50%,-50%) 偏移自身的宽和高的-50%*/
    transform: translate(-50%, -50%); 注意这里启动了3D硬件加速哦 会增加耗电量的 (至于何是3D加速 请看浏览器进程与线程篇)

4、flex 布局
    父级: 
        /*flex 布局*/
        display: flex;
        /*实现垂直居中*/
        align-items: center;
        /*实现水平居中*/
        justify-content: center;

再加一种水平方向上居中 :margin-left : 50% ; transform: translateX(-50%);

18、改变placeholder的字体颜色大小

其实这个方法也就在PC端可以,真机上屁用都没有,当时我就哭了。 但 还是贴出来吧

input::-webkit-input-placeholder { 
    /* WebKit browsers */ 
    font-size:14px;
    color: #333;
} 
input::-moz-placeholder { 
    /* Mozilla Firefox 19+ */ 
    font-size:14px;
    color: #333;
} 
input:-ms-input-placeholder { 
    /* Internet Explorer 10+ */ 
    font-size:14px;
    color: #333;
}

19、最快捷的数组求最大值

    var arr = [ 1,5,1,7,5,9];
    Math.max(...arr)  // 9 

20、更短的数组去重写法

    [...new Set([2,"12",2,12,1,2,1,6,12,13,6])]
    
    // [2, "12", 12, 1, 6, 13]

21、 vue 父子组件嵌套时,组件内部的各个生命周期钩子触发先后顺序

首先 我们可以把 子组件当做function函数来看待,当父组件 import 子组件的时候, 就当是声明了 并加载了这个函数,
在调用的时候才会去执行这个函数(子组件)。那么父子组件中的各个声明周期钩子触发的先后顺序是怎样的呢?
如下图:

下图带222 的 是为子组件,所以一次顺序是为 先创建父组件,然后才穿件子组件,当子组件创建完成并且实体dom挂载完成后父组件才挂载完成

注:资源来源于自己长期收集整理而来,如有和其他网站和论坛相同部分,在此抱歉!

查看原文

SmallW 关注了问题 · 2017-10-11

解决关于http和rpc的区别

最近用了谷歌的grpc,所以对rpc和http有一点疑惑,感觉这两个东西功能上是一样的,rpc某个服务监听某一个方法,客户端调用这个方法,返回相应的数据,
和http监听某个方法的路由 返回相应的数据好像没有本质上的区别啊,
求哪位前辈给细致的或浅显的大致的讲解一下这俩的区别 和所用的场景

关注 10 回答 5

SmallW 关注了标签 · 2017-04-11

docker

an open source project to pack, ship and run any application as a lightweight container ! By Lock !

关注 36778

SmallW 关注了问题 · 2017-01-22

各位大神,vue怎么检测窗口的大小变动

需求:窗口改变的时候,在组件中能监控到窗口的变化,从而改变控件的大小

关注 7 回答 4

认证与成就

  • 获得 3 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2016-01-12
个人主页被 414 人浏览