ziison

ziison 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 ziison.coding.me 编辑
编辑

化蝶后,莫作蛹中态

个人动态

ziison 收藏了文章 · 1月12日

Angular 2 DI - IoC & DI - 1

IoC 是什么

Ioc - Inversion of Control , 即"控制反转"。在开发中, IoC 意味着你设计好的对象交给容器控制,而不是使用传统的方式,在对象内部直接控制。  

如何理解好 IoC 呢?理解好 IoC的关键是要明确"谁控制谁,控制什么,为何是反转(有反转就应该有正转),哪些方面反转了",我们来深入分析一下。  

  • 谁控制谁,控制什么: 在传统的程序设计中,我们直接在对象内部通过 new 的方式创建对象,是程序主动创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 IoC 容器控制对象的创建;谁控制谁?当然是 IoC 容器控制了对象;控制什么?主要是控制外部资源获取。

  • 为何是反转了,哪些方面反转了: 有反转就有正转,传统应用程序是由我们自己在对象中主动控制去获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转了;哪些方面反转了?依赖对象的获取被反转了。

IoC 能做什么

Ioc 不是一种技术,只是一种思想,一个重要的面向对象编程法则,它能指导我们如何设计松耦合、更优良的系统。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器注入组合对象,所以对象之间是松散耦合,这样也便于测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。  

其实 IoC 对编程带来的最大改变不是从代码上,而是思想上,发生了"主从换位"的变化。应用程序本来是老大,要获取什么资源都是主动出击,但在 IoC思想中,应用程序就变成被动了,被动的等待 IoC 容器来创建并注入它所需的资源了。    

IoC 和 DI

DI - Dependency Injection,即"依赖注入":组件之间的依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。  

理解 DI 的关键是:"谁依赖了谁,为什么需要依赖,谁注入了谁,注入了什么",那我们来深入分析一下:  

  • 谁依赖了谁:当然是应用程序依赖 IoC 容器

  • 为什么需要依赖:应用程序需要 IoC 容器来提供对象需要的外部资源

  • 谁注入谁:很明显是 IoC 容器注入应用程序依赖的对象

  • 注入了什么:注入某个对象所需的外部资源(包括对象、资源、常量数据)

IoC 和 DI 有什么关系?其实它们是同一个概念的不同角度描述,由于控制反转的概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护依赖关系),所以 2004 年大师级人物 Martin Fowler 又给出了一个新的名字:"依赖注入",相对 IoC 而言,"依赖注入" 明确描述了被注入对象依赖 IoC 容器配置依赖对象。  

总的来说, 控制反转(Inversion of Control)是说创建对象的控制权发生转移,以前创建对象的主动权和创建时机由应用程序把控,而现在这种权利转交给 IoC 容器,它就是一个专门用来创建对象的工厂,你需要什么对象,它就给你什么对象。有了 IoC 容器,依赖关系就改变了,原先的依赖关系就没了,它们都依赖 IoC容器了,通过 IoC 容器来建立它们之间的关系。  

DI 在 angular1 中的应用  

angular1 中声明依赖项的方式有3种,分为如下:  

// 方式一: 使用 $inject annotation 方式
var fn = function (a, b) {};
fn.$inject = ['a', 'b'];

// 方式二: 使用 array-style annotations 方式
var fn = ['a', 'b', function (a, b) {}];

// 方式三: 使用隐式声明方式 
var fn = function (a, b) {}; // 不推荐

为了支持以上多种声明方式,angular1 内部使用 annotate 函数来解析依赖项,该函数的实现如下:

var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; // 匹配参数列表
var FN_ARG_SPLIT = /,/; // 参数分隔符
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; // 匹配参数项
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; // 去除 // 或 /**/注释

function extractArgs(fn) { // 抽取参数列表
  var fnText = fn.toString().replace(STRIP_COMMENTS, ''), // 去除注释
      args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS);
  return args;
}

function anonFn(fn) {
  var args = extractArgs(fn);
  if (args) {
    return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
  }
  return 'fn';
}

function annotate(fn, strictDi, name) {
  var $inject,
      argDecl,
      last;
      
  if (typeof fn === 'function') {
    if (!($inject = fn.$inject)) { // 判断是否使用$inject方式声明依赖项
      $inject = [];
      if (fn.length) {
        if (strictDi) { // 使用严格注入模式,即不能使用隐式声明方式
         // 函数名非字符串或为falsy值(如undefined、null),未设置时默认值为undefined 
          if (!isString(name) || !name) { 
            name = fn.name || anonFn(fn);
          }
          throw $injectorMinErr('strictdi',
            '{0} is not using explicit annotation and cannot be 
                 invoked in strict mode', name);
        }
        argDecl = extractArgs(fn); // 处理隐式声明方式
        forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
          arg.replace(FN_ARG, function(all, underscore, name) {
              $inject.push(name);
          });
        });
      }
      fn.$inject = $inject;
    }
  } else if (isArray(fn)) { // 使用 array-style annotations 方式
    last = fn.length - 1; // 获取fn函数
    assertArgFn(fn[last], 'fn');
    $inject = fn.slice(0, last); // 获取依赖项
  } else {
    assertArgFn(fn, 'fn', true);
  }
  return $inject; // 返回依赖数组
}

angular1 内部通过调用 annotate 函数,获取函数的依赖列表(即依赖数组)后,应该如何获取每个项对应的依赖对象呢?我们来进一步分析一下:  

假设我们使用 array-style annotations 方式声明 fn 函数: 

var fn = ['a', 'b', function (a, b) {}]

调用annotate函数后,我们获得 fn 的依赖列表,即返回 ['a','b']。

获取依赖列表后,我们就能够根据依赖项的名称来获取对应的依赖对象。因此,依赖名与依赖对象的存储方式应该是使用 Key - Value 的方式进行存储(在 ES5 中我们可以使用对象字面量,如 var cache = {} 实现 K-V 存储)。在 angular1 内部提供了一个 getService 方法,用来获取依赖对象。它的具体实现如下:  

var INSTANTIATING = {}, // 是否实例化中
    providerSuffix = 'Provider', // provider后缀
    path = []; // 依赖路径

var factory = function(serviceName, caller) { // 实例工厂
   var provider = providerInjector.get(serviceName + providerSuffix, caller);
   return instanceInjector.invoke(provider.$get, provider, undefined, 
        serviceName);
});

function getService(serviceName, caller) {
      if (cache.hasOwnProperty(serviceName)) { // 依赖对象已创建
        if (cache[serviceName] === INSTANTIATING) {// 判断是否存在循环依赖
          throw $injectorMinErr('cdep', 'Circular dependency found:   
              {0}',serviceName + ' <- ' + path.join(' <- '));
        }
        return cache[serviceName];
      } else { // 依赖对象未创建
        try {
          path.unshift(serviceName); // 用于跟踪依赖路径
          cache[serviceName] = INSTANTIATING;
          // 实例化 serviceName 对应的依赖对象并存储
          return cache[serviceName] = factory(serviceName, caller);
        } catch (err) {
          if (cache[serviceName] === INSTANTIATING) {
            delete cache[serviceName]; // 实例化失败,从缓存中移除
          }
          throw err;
        } finally {
          path.shift();
        }
      }
    }

通过 getService 的实现方式,我们可以知道,若依赖对象已存在,我们直接从缓存中获取,如果依赖对象不存在,我们通过调用 serviceName 对象的provider来创建依赖对象,然后保存在对象实例缓存中。这样的话,间接说明了一个问题,即在 angular1 中,所有的依赖对象都是单例。  

这里我们先稍微解释一下Provider,然后再来列举 angular1 DI系统存在的一些问题。  

什么是Provider ?在 angular1 中,Provider是一个包含 $get 属性的普通 JS 对象。创建 provider 有两种方式:  

// 方式一: 使用对象方式
module.provider('a',{
  $get: function () {
     return 42;
   }
});

// 方式二: 使用构造函数方式
module.provider('a', function AProvider() {
   this.$get = function() { return 42; };
});

以上两种方式都是使用 module 对象提供的provider方法来注册 provider,angular1 中 provider 的具体实现如下:  

function provider(name, provider_) {
    // provider 的名称不能为hasOwnProperty
    assertNotHasOwnProperty(name, 'service');
    // 构造函数方式,先进行实例化
    if (isFunction(provider_) || isArray(provider_)) {
      provider_ = providerInjector.instantiate(provider_);
    }
    if (!provider_.$get) { // 判断 provider_ 对象是否存在 $get属性
      throw $injectorMinErr('pget', "Provider '{0}' must define $get factory 
          method.", name);
    }
 // 使用 name + "Provider"作为 Key 值,保存在 providerCache 中,用于创建实例
    return providerCache[name + providerSuffix] = provider_;
  }

angular1 DI 系统存在的问题

  • 内部缓存: angular1 应用程序中所有的依赖项都是单例,我们不能控制是否使用新的实例

  • 命名空间冲突: 在系统中我们使用字符串来标识 service 的名称,假设我们在项目中已有一个 CarService,然而第三方库中也引入了同样的服务,这样的话就容易出现混淆

  • DI 耦合度太高: angular1 中 DI 功能已经被框架集成了,我们不能单独使用它的 DI 特性

  • 未能和模块加载器结合: 在浏览器环境中,很多场景都是异步的过程,我们需要的依赖模块并不是一开始就加载好的,或许我们在创建的时候才会去加载依赖模块,再进行依赖创建,而 angualr 的 IoC 容器没法做到这点。  

总结  

本文首先介绍了 IoC 和 DI 的概念及作用,然后讲述了 DI 在 angular1 中的实际应用。此外,简单的介绍了, angular1 DI 的实现方式,但并未深入介绍 angular1 中的 injector ,有兴趣的同学可以自行了解一下。最后,我们介绍了 angular1 DI 系统中存在的问题,这样为我们后面学习 angular2 DI 系统做好了铺垫,我们能更好地理解它设计的意图。

查看原文

ziison 收藏了文章 · 2020-12-02

Vue2.5+ Typescript 引入全面指南 - Vuex篇

Vue2.5+ Typescript 引入全面指南 - Vuex篇

系列目录:

前言

Vuex 正是我下决心引入Typescript的核心痛点。与 vuex 相关的代码中,到处充斥着此般写法:

Vuex Trouble 1

再加上vuex的 dispatch/commit 并非直接引用代码,而是是通过一个string类型的 type 来标记,如此一来上图中写法,如果想查看 payload 的具体内容,甚至不能借助于编辑器的查找定义,只能手动去切代码查看!简直苦不堪言。

而借助于 typescriptinterface 接口,至少可以简化成如下效果:

Vuex Payload Interface

这么写一样麻烦,不是还需要记类似PostLoginParams的Interface类型?这简单,建个辅助函数就是:

Vuex Payload Helper Function

编辑器里一个 Ctrl + 空格payload里有哪些参数就全出来,再也不用去一遍遍翻代码,效率直线提升!

Vuex dispatch Intellisense

现状概述

截至当前2017年11月,Vuex对Typescript的支持,仍十分薄弱,官方库只是添加了一些.d.ts声明文件,并没有像vue 2.5这样内置支持。

第三方衍生库 vuex-typescript, vuex-ts-decorators, vuex-typex, vuex-class等等,我个人的总结,除了vuex-class外,基本都存在侵入性太强的问题,引用不算友好。而vuex-class提供的功能其实也是薄薄一层,并不能解决核心痛点。因此,需要手动添加辅助的地方,其实颇多。

核心痛点:每次调用 this.$store.dispatch / this.$store.commit / this.$store.state/ this.$store.getters 都会伴随着类型丢失。

其中,dispatch/commit 可以通过建立辅助函数形式,简单绕开。 state/getters 没有太好办法,只能手动指定,若觉得麻烦,可以全都指成 any,等官方支持。官方动态见此 issue

动手改造第一步:从 shopping-cart 示例搬运代码

以下示例基于 vuex 官方 examples 中最复杂的一个 shopping-cart
改造后的完整代码见 vue-vuex-typescript-demo

准备工作:

  • shopping-cart代码复制至项目目录下
  • .js文件统一重命名为.ts
  • currency.js/api/shop.js/components/App.vue等外围文件的ts改造
  • npm i -D vuex 添加依赖

详细步骤这里略去,参照 代码库 即可

动手改造第二步:State改造

用到state变量的地方实在太多,不仅store目录下 action/getter/mutation 均有可能需要,甚至在 .vue 文件里,mapState也有引用,因此我个人总结的一套实践:

  • store/modules下的每个子模块,均维护自己名为 State 的 Interface 声明
  • store/index.ts 文件中,汇总各子模块,维护一个总的State声明

store/modules 下文件举例:

// ./src/store/modules/cart.ts

interface Shape {
  id: number
  quantity: number
}

export interface State {
  added: Shape[]
  checkoutStatus: 'successful' | 'failed' | null
}

// initial state
// shape: [{ id, quantity }]
const state: State = {
  added: [],
  checkoutStatus: null
}

// 需引用state的地方举例:

const getters = {
  checkoutStatus: (state: State) => state.checkoutStatus
}

store/index.ts 文件总 State 举例:

// ./src/store/index.ts

import { State as CardState } from './modules/cart'
import { State as ProductsState } from './modules/products'

export interface State {
  cart: CardState,
  products: ProductsState
}

State 引用示例:

// ./src/store/getters.ts

import { State } from './index'

const cartProducts: Getter<State, any> = (state: State) => {
  return state.cart.added.map(shape => {
    // 此处shape自动推导出Shape类型
    // ... 详见源码
  })
}

如此,所有直接引用 state 的地方,均可启用类型推导

动手改造之 Mutation

Mutation 对应 store.commit 命令,常见写法:

const mutations = {
  [types.ADD_TO_CART] (state, { id }) {
    // ...
  }
}

state 上步已处理{ id }payload 参数,即为开篇介绍类型缺失的重灾区。

我的一套个人实践:

  • store/modules 下的子模块文件,为自己的mutations 维护 payload Interface声明
  • 子模块共用 payload(多个模块响应同一 commit 等),在 store/index.ts 中统一维护
  • 新建文件 store/dispatches.ts 文件,为每一个直接调用的带参commit维护辅助函数,以应用类型推导

子模块 payload 声明举例:

// ./src/store/modules/products.ts

import { Product, AddToCartPayload } from '../index'

export interface ProductsPayload {
  products: Product[]
}

const mutations = {
  [types.RECEIVE_PRODUCTS] (state: State, payload: ProductsPayload) {
    state.all = payload.products
  },

  [types.ADD_TO_CART] (state: State, payload: AddToCartPayload) {
    const product = state.all.find(p => p.id === payload.id)
    // ...
  }
}

// mutations调用举例:
const actions = {
  getAllProducts (context: ActionContextBasic) {
    shop.getProducts((products: Product[]) => {
      const payload: ProductsPayload = {
        products
      }
      context.commit(types.RECEIVE_PRODUCTS, payload)
    })
  }
}

store/index.ts文件公共 payload 声明举例:

// ./src/store/index.ts

export interface AddToCartPayload {
  id: number
}

store/dispatches.ts文件,commit辅助函数,参见下步同文件dispatch辅助函数

动手改造之 Action

Action 对应 store.dispatch 命令,常见写法:

const actions = {
  checkout ({ commit, state }, products) {
    // ...
  }
}

其中第二个参数productspayload 参数,用法同上步 Mutationpayload 参数,不再赘述。

第一个参数{ commit, state }context参数,vuexd.ts 提供有类型 ActionContext,用法如下:

import { ActionContext } from 'vuex'
const actions = {
  checkout (context: ActionContext<State, any>, products: CartProduct[]) {
    context.commit(types.CHECKOUT_REQUEST)
    // ...
  }
}

ActionContext<State, RootState> 传入两个大部分Action根本用不到的参数,才能得到需要的dispatch, commit,在我看来,难用至极。

个人更喜欢如下写法:

const actions = {
  checkout (context: { commit: Commit, state: State }, products: CartProduct[]) {
    context.commit(types.CHECKOUT_REQUEST)
    // ...
  }
}

Action payload 改造参见步骤 Mutation,不再赘述。

store/dispatches.ts文件,dispatch辅助函数:

// ./src/store/dispatches.ts

import store, { CartProduct, Product } from './index'

export const dispatchCheckout = (products: CartProduct[]) => {
  return store.dispatch('checkout', products)
}

.vue文件调用举例:

// ./src/components/Cart.vue

import { dispatchCheckout } from '../store/dispatches'
export default Vue.extend({
  methods: {
    checkout (products: CartProduct[]) {
    // this.$store.dispatch 写法可用,但不带类型推导
    // this.$store.dispatch('checkout', products)
    dispatchCheckout(products) // 带有类型智能提示
    }
  }
})

动手改造之 Getter

Getter常见写法:

const getters = {
  checkoutStatus: state => state.checkoutStatus
}

需要改的不多,state 加上声明即可:

const getters = {
  checkoutStatus: (state: State) => state.checkoutStatus
}

动手改造之独立的 Mutations/Actions/Getters 文件

独立文件常规写法:

// ./src/store/getters.js
export const cartProducts = state => {
  return state.cart.added.map(({ id, quantity }) => {
    const product = state.products.all.find(p => p.id === id)
    return {
      title: product.title,
      price: product.price,
      quantity
    }
  })
}

引用:

// ./src/store/index.js

import * as getters from './getters'
export default new Vuex.Store({
  getters
})

typescript下均需改造:

// ./src/
import { GetterTree, Getter } from 'vuex'
import { State } from './index'

const cartProducts: Getter<State, any> = (state: State) => {
  return state.cart.added.map(shape => {
    // ...
  })
}

const getterTree: GetterTree<State, any> = {
  cartProducts
}

export default getterTree

Actions/Mutations 文件改造同上,类型换成 ActionTree, Action, MutationTree, Mutation即可

引用:

// ./src/store/index.js

import getters from './getters'
export default new Vuex.Store({
  getters
})

原因是vuex定义,new Vuex.Store参数类型 StoreOptions 如下:

export interface StoreOptions<S> {
  state?: S;
  getters?: GetterTree<S, S>;
  actions?: ActionTree<S, S>;
  mutations?: MutationTree<S>;
  modules?: ModuleTree<S>;
  plugins?: Plugin<S>[];
  strict?: boolean;
}

于是,独立Gettes/Actions/Mutations文件,export 必须是GetterTree/ActionTree/MutationTree类型

动手改造之 .vue 文件调用

  • 传统写法全部兼容,只需 mapState为state添加类型 (state: State) => state.balabal 等很少改动即可正常运行。只是类型均为 any
  • 建议不使用 mapState / mapGetters / mapActions / mapMutations,以明确指定类型
  • dispatchcommit 调用可通过上述 store/dispatches.ts 下辅助函数,手动开启类型推导
  • stategetters 类型推导,暂时只能手动指定。自动推导,估计得等官方内置支持了。

完整调用示例:

// ./src/components/ProductList.vue

import Vue from 'vue'
// import { mapGetters, mapActions } from 'vuex'
import { Product } from '../store'
import { dispatchAddToCart } from '../store/dispatches'

export default Vue.extend({
  computed: {
    // ...mapGetters({
    //   products: 'allProducts'
    // })
    products (): Product[] {
      return this.$store.getters.allProducts
    }
  },
  methods: {
    // ...mapActions([
    //   'addToCart'
    // ])
    addToCart (p: Product) {
      dispatchAddToCart(p)
    }
  },
  created () {
    this.$store.dispatch('getAllProducts')
  }
})

vue-class-component + vuex-class 组件式写法

如果觉得以上废弃 mapState / mapGetters 后的写法繁琐,可引入vue-class-component + vuex-class,开启组件式写法

  • vue-class-component,vue官方维护,学习成本低
  • vuex-class,作者 ktsn,vuex及vue-class-component贡献排第二(第一尤雨溪了)的活跃开发者,质量还是有保障的

引入这俩依赖后,须在 tsconfig.json 添加配置:

{
  "compilerOptions": {
    // 启用 vue-class-component 及 vuex-class 需要开启此选项
    "experimentalDecorators": true,

    // 启用 vuex-class 需要开启此选项
    "strictFunctionTypes": false
  }
}

Component 写法示例:

import Vue from 'vue'
import { Product } from '../store'
// import { dispatchAddToCart } from '../store/dispatches'
import Component from 'vue-class-component'
import { Getter, Action } from 'vuex-class'

@Component
export default class Cart extends Vue {
  @Getter('cartProducts') products: CartProduct[]
  @Getter('checkoutStatus') checkoutStatus: CheckoutStatus
  @Action('checkout') actionCheckout: Function

  get total (): number {
    return this.products.reduce((total, p) => {
      return total + p.price * p.quantity
    }, 0)
  }

  checkout (products: CartProduct[]) {
    // dispatchCheckout(products)
    this.actionCheckout(products)
  }
}

总结

在现阶段 vuex 官方未改进 typescript 支持下,用 typescript 写 vuex 代码,的确有些繁琐,而且支持也称不上全面,不过,总比没有强。哪怕都用 any,也能借助智能提示减轻一些代码翻来翻去的痛苦。

至于再进一步更完美的支持,等官方更新吧。

完整代码

见 Github 库:vue-vuex-typescript-demo

查看原文

ziison 关注了标签 · 2020-10-15

typescript

TypeScript 是微软开发的 JavaScript 的超集,TypeScript兼容JavaScript,可以载入JavaScript代码然后运行。TypeScript与JavaScript相比进步的地方。包括:加入注释,让编译器理解所支持的对象和函数,编译器会移除注释,不会增加开销;增加一个完整的类结构,使之更新是传统的面向对象语言。

关注 32391

ziison 收藏了文章 · 2020-07-28

让你的 commit 更有价值

一个持续更新的github笔记,链接地址:Front-End-Basics,本文地址:让你的 commit 更有价值


提交规范

AngularJS 在开发者文档中关于 git commit 的指导说明,提到严格的 git commit 格式规范可以在浏览项目历史的过程中看到更易读的信息,并且能用 git commit 的信息直接生成 AngularJS 的 change log 。

commit messages 格式规范

commit messages 由 headerbodyfooter 组成。

header 又包含 typescopesubjectheader 是必需的,不过其中的 scope 是可选的。

bodyfooter 可以省略。

<type>(<scope>): <subject>
// 空行
<BLANK LINE>
<body>
// 空行
<BLANK LINE>
<footer>
注:为了能在 github 以及各种 git 工具中看得更清晰,commit messages 的每一行都不要超过 100 个字符。

Header

Type

类型必须是以下几种之一:

  • feat: 新功能
  • fix: bug 修复
  • docs: 仅修改文档
  • style: 修改格式(空格,格式化,省略分号等),对代码运行没有影响
  • refactor: 重构(既不是修 bug ,也不是加功能)
  • build: 构建流程、外部依赖变更,比如升级 npm 包、修改 webpack 配置等
  • perf: 性能优化
  • test: 测试相关
  • chore: 对构建过程或辅助工具和库(如文档生成)的更改
  • ci: ci 相关的更改

除此之外,还有一个特殊的类型 revert ,如果当前提交是为了撤销之前的某次提交,应该用 revert 开头,后面加上被撤销的提交的 header,在 body 中应该注明: This reverts commit <hash>. ,hash 指的就是将要被撤销的 commit SHA 。

// 例如

revert: feat(user): add user type

This reverts commit ca16a365467e17915f0273392f4a13331b17617d.

Scope

scope 可以指定提交更改的影响范围,这个视项目而定,当修改影响超过单个的 scope 时,可以指定为 *

Subject

subject 是指更改的简洁描述,长度约定在 50 个字符以内,通常遵循以下几个规范:

  • 用动词开头,第一人称现在时表述,例如:change 代替 changedchanges
  • 第一个字母小写
  • 结尾不加句号(.)

Body

body 部分是对本地 commit 的详细描述,可以分成多行。

subject 类似,用动词开头,第一人称现在时表述,例如:change 代替 changedchanges

body 应该说明修改的原因和更改前后的行为对比。

Footer

footer 基本用在这两种情况:

  • 不兼容的改动( Breaking Changes ),通常用 BREAKING CHANGE: 开头,后面跟一个空格或两个换行符。剩余的部分就是用来说明这个变动的信息和迁移方法等。
  • 关闭 Issue, github 关闭 Issue 的例子
// BREAKING CHANGE: 的例子
BREAKING CHANGE: isolate scope bindings definition has changed and
    the inject option for the directive controller injection was removed.

    To migrate the code follow the example below:

    Before:

    scope: {
      myAttr: 'attribute',
      myBind: 'bind',
      myExpression: 'expression',
      myEval: 'evaluate',
      myAccessor: 'accessor'
    }

    After:

    scope: {
      myAttr: '@',
      myBind: '@',
      myExpression: '&',
      // myEval - usually not useful, but in cases where the expression is assignable, you can use '='
      myAccessor: '=' // in directive's template change myAccessor() to myAccessor
    }

    The removed `inject` wasn't generaly useful for directives so there should be no code using it.



// Closes Issue 例子
Closes #2314, #3421

完整的例子

例一: feat

feat($browser): onUrlChange event (popstate/hashchange/polling)

Added new event to $browser:
- forward popstate event if available
- forward hashchange event if popstate not available
- do polling when neither popstate nor hashchange available

Breaks $browser.onHashChange, which was removed (use onUrlChange instead)

例二: fix

fix($compile): couple of unit tests for IE9

Older IEs serialize html uppercased, but IE9 does not...
Would be better to expect case insensitive, unfortunately jasmine does
not allow to user regexps for throw expectations.

Closes #392
Breaks foo.bar api, foo.baz should be used instead

例三: style

style($location): add couple of missing semi colons

查看更多例子

规范 commit message 的好处

  • 首行就是简洁实用的关键信息,方便在 git history 中快速浏览
  • 具有详实的 body 和 footer ,可以清晰的看出某次提交的目的和影响
  • 可以通过 type 过滤出想要查找的信息,也可以通过关键字快速查找相关提交
  • 可以直接从 commit 生成 change log
// 列举几个常用的 log 参数

// 输出 log 的首行
git log --pretty=oneline

// 只输出首行的 commit 信息。不包含 hash 和 合并信息等
git log --pretty=format:%s

// 查找有关“更新菜单配置项”的提交
git log --grep="更新菜单配置项"

// 打印出 chenfangxu 的提交
git log --author=chenfangxu

// 红色的短 hash,黄色的 ref , 绿色的相对时间
git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset'

用工具实现规范提交

上面介绍了规范提交的格式,如果让各位同学在 git commit 的时候严格按照上面的规范来写,首先心智是有负担的,得记住不同的类型到底是用来定义什么的,subject 怎么写,body 怎么写,footer 要不要写。其次,对人的规范大部分都是反人性的,所以很可能在过不了多久,就会有同学渐渐的不按照规范来写。靠意志力来控制自己严格按照规范来写是需要额外耗费一些精力的,把精力耗费在这种事情上面实在有些浪费。

用工具实现规范提交的方案,一种是在提交的时候就提示必填字段,另一种是在提交后校验字段是否符合规范。这两种在实际项目中都是很有必要的。

Commitizen

Zen-like commit messages for internet citizens. 嗯~~一种禅意

Commitizen 是一个帮助撰写规范 commit message 的工具。他有一个命令行工具 cz-cli,接下来会把使用 Commitizen 分成几个阶段来介绍。

体验 git cz

// 全局安装 Commitizen
npm install -g commitizen

你的仓库可能还不是对 Commitizen 友好的,此时运行 git cz 的效果跟 git commit 一样,也就是没有效果。 不过,可以执行 npx git-cz 来体验。

如果想直接运行 git cz 实现语义化的提交,可以根据 streamich/git-cz 文档中说的全局安装 git cz

// 全局安装 git cz
npm install -g git-cz

除此之外还有一种更推荐的方式,就是让你的仓库对 Commitizen 友好。

Commitizen 友好

全局安装 Commitizen 后,用 cz-conventional-changelog 适配器来初始化你的项目

// 初始化 cz-conventional-changelog 适配器
commitizen init cz-conventional-changelog --save-dev --save-exact

上面的初始化做了三件事:

  • 安装 cz-conventional-changelog 依赖
  • 把依赖保存到 package.json 的 dependenciesdevDependencies
  • 在根目录的 package.json 中 添加如下所示的 config.commitizen
"config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  }

或者,在项目根目录下新建一个 .czrc 文件,内容设置为

{
  "path": "cz-conventional-changelog"
}

现在运行 git cz 效果如下:

cz-customizable 自定义中文配置

通过上面的截图可以看到,提交的配置选项都是英文的,如果想改成中文的,可以使用 cz-customizable 适配器。

运行下面的命令,注意之前已经初始化过一次了,这次再初始化,需要加 --force 覆盖

npm install cz-customizable --save-dev

commitizen init cz-customizable --save-dev --save-exact --force

现在 package.json 中 config.commitizen 字段为:

"config": {
    "commitizen": {
      "path": "./node_modules/cz-customizable"
    }
  }

cz-customizable 文档中说明了查找配置文件的方式有三种,我们按照第一种,在项目根目录创建一个 .cz-config.js 的文件。按照给出的示例 cz-config-EXAMPLE.js 编写我们的 config。 commit-type 可以参考 conventional-commit-types

可以点击查看我配置好的文件 qiqihaobenben/commitizen-git/.cz-config.js ,里面中详细的注释。

commitlint 校验提交

Commitizen 文档中开始就介绍到,Commitizen 可以在触发 git commit 钩子之前就能给出提示,但是也明确表示提交时对 commit messages 的校验也是很有用的。毕竟即使用了 Commitzen,也是能绕过去,所以提交最后的校验很重要。

commitlint 可以检查 commit messages 是否符合常规提交格式,需要一份校验配置,推荐 @commitlint/config-conventional

npm i --save-dev @commitlint/config-conventional @commitlint/cli

在项目根目录创建 commitlint.config.js 文件并设置校验规则:

module.exports = {
  extends: ["@commitlint/config-conventional"],
  // rules 里面可以设置一些自定义的校验规则
  rules: {},
};

在项目中安装 husky ,并在项目根目录新建 husky.config.js 文件,加入以下设置:

// 安装 husky
npm install --save-dev husky


// husky.config.js 中加入以下代码
module.exports = {
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  }
}
注意:因为 @commitlint/config-conventional 校验规则遵循 Angular 的规范, 所以我们在用 cz-customizable 自定义中文配置时, 是按照给出的符合 Angular 规范的示例 cz-config-EXAMPLE.js 编写.cz-config.js 的。但是如果你自定义的 Commitizen 配置不符合 Angular 规范,可以使用 commitlint-config-cz 设置校验规则。(推荐还是按照 Angular 规范进行 cz-customizable 自定义配置)
// 安装 commitlint-config-cz
npm install commitlint-config-cz --save-dev


// commitlint.config.js 改为
module.exports = {
  extends: [
    'cz'
  ]
};

git commit 触发 git cz

在提交的时候,我们都习惯了 git commit ,虽然换成 git cz 不难,但是如果让开发者在 git commit 时无感知的触发 git cz 肯定是更好的,
而且也能避免不熟悉项目的人直接 git commit 提交一些不符合规范的信息。

我们可以在 husky.config.js 中设置:

"hooks": {
  "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true",
}
注意: 在 window 系统,可能需要在 git base 中才能生效。

生成 CHANGELOG

standard-version
是一个使用 semverconventional-commits 支持生成 CHANGELOG 进行版本控制的实用程序。
standard-version 不只是能生成 CHANGELOG , 还能根据 commit 的 type 来进行版本控制。

// 安装 standard-verison
npm i --save-dev standard-version

// 在 package.json 中的 scripts 加入 standard-version
{
  "scripts": {
    "release": "standard-version"
  }
}

示例项目

可以查看 commitizen-git ,里面归纳了快速配置 Commitizen 友好仓库的步骤。
差不多三五分钟就能搞定。

可以看一下配置完后,执行 git commit 的效果。

扩展

更复杂的自定义提示

cz-customizable 中自定义配置项通常情况是够用的,commitlint 中校验的规则基本上也是够用的,但是会有比较硬核的开发者会觉得还是不够,还要更多。比如一些 prompt 更加自定义,提交时询问的question 添加更多的逻辑,比如可以把一些重要的字段校验提前到 Commitizen 中,或者添加更多自定义的校验。

如果真想这么干,那就去 fork 一份 cz-conventional-changelog 或者 cz-customizable 来改,或者直接自己写一个 adapter。

Commitizen 友好徽章

如果把仓库配置成了对 Commitizen 友好的话,可以在 README.md 中加上这个小徽章:Commitizen friendly

参考文档

查看原文

ziison 收藏了文章 · 2020-01-17

React新特性实例详解(memo、lazy、suspense)

1. memo
介绍React.memo之前,先了解一下React.Component和React.PureComponent。

React允许定义一个class或者function作为组件,那么定义一个组件类,就需要继承React.Component.

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
注意:继承React.Component的React组件类中,render()为必须方法,其他都为可选。

React.PureComponent 和 React.Component类似,都是定义一个组件类。不同是React.Component没有实现shouldComponentUpdate(),而 React.PureComponent通过props和state的浅比较实现了。

如果组件的props和state相同时,render的内容也一致,那么就可以使用React.PureComponent了,这样可以提高组件的性能。

class Welcome extends React.PureComponent {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
当props和state中有复杂数据结果时,不好使用PureComponent,还是要使用Component。

React.memo是一个高阶组件,类似于React.PureComponent,不同于React.memo是function组件,React.PureComponent是class组件。

const MyComponent = React.memo(props => {
  return (

  );
});

这种方式依然是一种对象的浅比较,有复杂对象时无法render。在React.memo中可以自定义其比较方法的实现。
memo接收两个参数,一个是组件,一个是函数。这个函数就是定义了memo需不需要render的钩子。
比较前一次的props跟当前props,返回true表示不需要render。
也就是传给Memo的name不变时,不会触发MyComponent的render函数。

function MyComponent(props) {
  return (

  );
}
function areEqual(prevProps, nextProps) {
  // 就是判断props的变化对UI的影响
  if(prevProps === nextProps) {
    return true;      
  }
  return falae;
}
export default React.memo(MyComponent, areEqual);

2.lazy and suspence
动态导入主要应用场景是延迟加载方法,对于组件来说,并不是很适用,但是React.lazy对于组件的加载则是有比较大的帮助。
注意:目前明确指出,React.lazy和suspense并不适用于服务端渲染
既然是延迟加载,就会有一个加载过程,之前在渲染的时候,基本我们自都是顶一个一个<Loading>组件,然后通过变量控制进行操作,如果加载完成,取消掉则<Loading>组件。

如果直接使用React.lazy,会报错误:需要一个占位符ui,
Suspense使用的时候,fallback一定是存在且有内容的,否则会报错。
代码如下:

import React, { Component, Suspense, lazy } from 'react';
const Other1 = lazy(() => import('./other'));
const Other2 = lazy(() => new Promise(resolve =>
  setTimeout(() =>
    resolve(
      // 模拟ES Module
      {
        // 模拟export default 
        default: function render() {
          return <div>Other2 Component</div>
        }
      }
    ),
    3000
  )
));
class App extends Component {
  render() {
    return (
      <div>
        <h4>一个基本的 lazy 和 suspense 的例子</h4>
        <Suspense fallback={<div>Other1 Loading...</div>}>
          <Other1 />
        </Suspense>
        <h4>一个模拟 lazy 和 suspense 的例子</h4>
        <Suspense fallback={<div>Other2 Loading...</div>}>
          <Other2 />
        </Suspense>
      </div>
    );
  }
}

export default App;
查看原文

ziison 收藏了文章 · 2020-01-15

Styled-Components

Styled-Components

它是通过JavaScript改变CSS编写方式的解决方案之一,从根本上解决常规CSS编写的一些弊端。
通过JavaScript来为CSS赋能,我们能达到常规CSS所不好处理的逻辑复杂、函数方法、复用、避免干扰。
尽管像SASS、LESS这种预处理语言添加了很多用用的特性,但是他们依旧没有对改变CSS的混乱有太大的帮助。因此组织工作交给了像 BEM这样的方法,虽然比较有用,但是它完全是自选方案,不能被强制应用在语言或者工具层面。
他搭配React可能将模块化走向一个更高的高度,样式书写将直接依附在JSX上面,HTML、CSS、JS三者再次内聚。

基本

安装

npm install --save styled-components

除了npm安装使用模块化加载包之外,也支持UMD格式直接加载脚本文件。

<script data-original="https://unpkg.com/styled-components/dist/styled-components.min.js"></script>

入门

styled-components使用标签模板来对组件进行样式化。

它移除了组件和样式之间的映射。这意味着,当你定义你的样式时,你实际上创造了一个正常的React组件,你的样式也附在它上面。

这个例子创建了两个简单的组件,一个容器和一个标题,并附加了一些样式。

// Create a Title component that'll render an <h1> tag with some styles
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

// Create a Wrapper component that'll render a <section> tag with some styles
const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;

// Use Title and Wrapper like any other React component – except they're styled!
render(
  <Wrapper>
    <Title>
      Hello World, this is my first styled component!
    </Title>
  </Wrapper>
);
注意
CSS规则会自动添加浏览器厂商前缀,我们不必考虑它。

透传props

styled-components会透传所有的props属性。

// Create an Input component that'll render an <input> tag with some styles
const Input = styled.input`
  padding: 0.5em;
  margin: 0.5em;
  color: palevioletred;
  background: papayawhip;
  border: none;
  border-radius: 3px;
`;

// Render a styled text input with a placeholder of "@mxstbr", and one with a value of "@geelen"
render(
  <div>
    <Input placeholder="@mxstbr" type="text" />
    <Input value="@geelen" type="text" />
  </div>
);

基于props做样式判断

模板标签的函数插值能拿到样式组件的props,可以据此调整我们的样式规则。

const Button = styled.button`
  /* Adapt the colours based on primary prop */
  background: ${props => props.primary ? 'palevioletred' : 'white'};
  color: ${props => props.primary ? 'white' : 'palevioletred'};

  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

render(
  <div>
    <Button>Normal</Button>
    <Button primary>Primary</Button>
  </div>
);

样式化任意组件

// This could be react-router's Link for example
const Link = ({ className, children }) => (
  <a className={className}>
    {children}
  </a>
)

const StyledLink = styled(Link)`
  color: palevioletred;
  font-weight: bold;
`;

render(
  <div>
    <Link>Unstyled, boring Link</Link>
    <br />
    <StyledLink>Styled, exciting Link</StyledLink>
  </div>
);

扩展样式

我们有时候需要在我们的样式组件上做一点扩展,添加一些额外的样式:
需要注意的是.extend在对样式组件有效,如果是其他的React组件,需要用styled样式化一下。

// The Button from the last section without the interpolations
const Button = styled.button`
  color: palevioletred;
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

// We're extending Button with some extra styles
const TomatoButton = Button.extend`
  color: tomato;
  border-color: tomato;
`;

render(
  <div>
    <Button>Normal Button</Button>
    <TomatoButton>Tomato Button</TomatoButton>
  </div>
);

在极少特殊情况下,我们可能需要更改样式组件的标签类型。我们有一个特别的API,withComponent可以扩展样式和替换标签:

const Button = styled.button`
  display: inline-block;
  color: palevioletred;
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

// We're replacing the <button> tag with an <a> tag, but reuse all the same styles
const Link = Button.withComponent('a')

// Use .withComponent together with .extend to both change the tag and use additional styles
const TomatoLink = Link.extend`
  color: tomato;
  border-color: tomato;
`;

render(
  <div>
    <Button>Normal Button</Button>
    <Link>Normal Link</Link>
    <TomatoLink>Tomato Link</TomatoLink>
  </div>
);

添加attr

我们可以使用attrsAPI来为样式组件添加一些attr属性,它们也可以通过标签模板插值函数拿到props传值。

const Input = styled.input.attrs({
  // we can define static props
  type: 'password',

  // or we can define dynamic ones
  margin: props => props.size || '1em',
  padding: props => props.size || '1em'
})`
  color: palevioletred;
  font-size: 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;

  /* here we use the dynamically computed props */
  margin: ${props => props.margin};
  padding: ${props => props.padding};
`;

render(
  <div>
    <Input placeholder="A small text input" size="1em" />
    <br />
    <Input placeholder="A bigger text input" size="2em" />
  </div>
);

动画

带有@keyframes的CSS animations,一般来说会产生复用。styled-components暴露了一个keyframes的API,我们使用它产生一个可以复用的变量。这样,我们在书写css样式的时候使用JavaScript的功能,为CSS附能,并且避免了名称冲突。

// keyframes returns a unique name based on a hash of the contents of the keyframes
const rotate360 = keyframes`
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
`;

// Here we create a component that will rotate everything we pass in over two seconds
const Rotate = styled.div`
  display: inline-block;
  animation: ${rotate360} 2s linear infinite;
  padding: 2rem 1rem;
  font-size: 1.2rem;
`;

render(
  <Rotate>&lt; 💅 &gt;</Rotate>
);

支持 React Native

高级特性

Theming

styled-components暴露了一个<ThemeProvider>容器组件,提供了设置默认主题样式的功能,他类似于react-rudux的顶层组件Provider,通过context实现了从顶层到底层所有样式组件的默认主题共用。

const Button = styled.button`
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border-radius: 3px;
  
  /* Color the border and text with theme.main */
  color: ${props => props.theme.main};
  border: 2px solid ${props => props.theme.main};
`;

Button.defaultProps = {
  theme: {
    main: 'palevioletred'
  }
}
// Define what props.theme will look like
const theme = {
  main: 'mediumseagreen'
};

render(
  <div>
    <Button>Normal</Button>
    <ThemeProvider theme={theme}>
      <Button>Themed</Button>
    </ThemeProvider>
  </div>
);

Refs

通常我们在给一个非原生样式组件添加ref属性的时候,其指向都是该组件实例的索引,我们通过用innerRef可以直接拿到里面的DOM节点。

const AutoFocusInput = styled.input`
  background: papayawhip;
  border: none;
`;

class Form extends React.Component {
  render() {
    return (
      <AutoFocusInput
        placeholder="Hover here..."
        innerRef={x => { this.input = x }}
        onMouseEnter={() => this.input.focus()}
      />
    );
  }
}

Security

因为styled-components允许我们使用任意输入作为CSS属性值,一旦意识到这一点,我们马上明白要对输入做安全性校验了,因为使用用户外部的输入样式可以导致用户的浏览器被CSS注入攻击。CSS注入攻击可能不明显,但是我们还是得小心一点,某些IE浏览器版本甚至允许在URL声明中执行任意的JS。

这个例子告诉我们外部的输入甚至可能在CSS内调用一个API网络请求。

// Oh no! The user has given us a bad URL!
const userInput = '/api/withdraw-funds';

const ArbitraryComponent = styled.div`
  background: url(${userInput});
  /* More styles here... */
`;

CSS.escape这个未来API标准可净化JS中的CSS的问题。但是浏览器兼容性目前还不是太好,所以我们建议在项目中使用polyfill by Mathias Bynens

CSS共存

如果我们打算把styled-components和现有的css共存的话,我们需要注意两个实现的细节问题:

styled-components也会生成真实的样式表,并通过className属性链接生成的样式表内容。在JS运行时,他会生成一份真实的style节点插入到document的head内。

注意的一个小地方:

// MyComponent.js
const MyComponent = styled.div`background-color: green;`;

// my-component.css
.red-bg {
  background-color: red;
}

// For some reason this component still has a green background,
// even though you're trying to override it with the "red-bg" class!
<MyComponent className="red-bg" />

我们styled-components生成的style样式表一般是在head头部的最底下,同等CSS优先级条件下是会覆盖默认前者css文件的样式的。这个插入顺序使用webpack来调整是比较难得。所以,我们一般都这样通过调整css优先级来改变显示:

/* my-component.css */
.red-bg.red-bg {
  background-color: red;
}

Media Templates

媒体查询是开发响应式web应用不可或缺的存在,这是一个简单的例子:

const Content = styled.div`
  background: papayawhip;
  height: 3em;
  width: 3em;

  @media (max-width: 700px) {
    background: palevioletred;
  }
`;

render(
  <Content />
);

因为媒体查询语句很长,并且经常在整个应用程序中重复使用,所以为此创建一些模板来复用是很有必要的。

使用JS的功能特性,我们可以轻松定义一份可配置的语句,包装媒体查询和样式。

const sizes = {
  desktop: 992,
  tablet: 768,
  phone: 376
}

// Iterate through the sizes and create a media template
const media = Object.keys(sizes).reduce((acc, label) => {
  acc[label] = (...args) => css`
    @media (max-width: ${sizes[label] / 16}em) {
      ${css(...args)}
    }
  `

  return acc
}, {})

const Content = styled.div`
  height: 3em;
  width: 3em;
  background: papayawhip;

  /* Now we have our methods on media and can use them instead of raw queries */
  ${media.desktop`background: dodgerblue;`}
  ${media.tablet`background: mediumseagreen;`}
  ${media.phone`background: palevioletred;`}
`;

render(
  <Content />
);

这太cool了,不是吗?

Tagged Template Literals

标签模板是ES6的一个新特性,这是我们styled-components创建样式组件的方式和规则。

const aVar = 'good';

// These are equivalent:
fn`this is a ${aVar} day`;
fn([ 'this is a ', ' day' ], aVar);

这看起来有点麻烦,但是这意味着我们可以在styled-components生成样式组件中接受变量、函数、minxins,并将其变为纯css。

这篇文章可以了解更多:The magic behind 💅 styled-components

Server Side Rendering

styled-components很好地支持SSR。

一个例子:

import { renderToString } from 'react-dom/server'
import { ServerStyleSheet } from 'styled-components'

const sheet = new ServerStyleSheet()
const html = renderToString(sheet.collectStyles(<YourApp />))
const styleTags = sheet.getStyleTags() // or sheet.getStyleElement()

也可以这样组件化包裹,只要在客户端不这么使用:

import { renderToString } from 'react-dom/server'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'

const sheet = new ServerStyleSheet()
const html = renderToString(
  <StyleSheetManager sheet={sheet.instance}>
    <YourApp />
  </StyleSheetManager>
)

const styleTags = sheet.getStyleTags() // or sheet.getStyleElement()

sheet.getStyleTags()返回一个style标签数组。具体styled-components关于SSR更深入的操作,不在这里继续讨论了,还可以告知他兼容Next.js关于SSR的解决方案。

Referring to other components

styled-components提供了component selector组件选择器模式来代替我们以往对class名的依赖,解决得很干净。这下我们不必为命名和选择器冲突而苦恼了。

const Link = styled.a`
  display: flex;
  align-items: center;
  padding: 5px 10px;
  background: papayawhip;
  color: palevioletred;
`;

const Icon = styled.svg`
  transition: fill 0.25s;
  width: 48px;
  height: 48px;

  ${Link}:hover & {
    fill: rebeccapurple;
  }
`;

const Label = styled.span`
  display: flex;
  align-items: center;
  line-height: 1.2;

  &::before {
    content: '◀';
    margin: 0 10px;
  }
`;

render(
  <Link href="#">
    <Icon viewBox="0 0 20 20">
      <path d="M10 15h8c1 0 2-1 2-2V3c0-1-1-2-2-2H2C1 1 0 2 0 3v10c0 1 1 2 2 2h4v4l4-4zM5 7h2v2H5V7zm4 0h2v2H9V7zm4 0h2v2h-2V7z"/>
    </Icon>
    <Label>Hovering my parent changes my style!</Label>
  </Link>
);

注意:

class A extends React.Component {
  render() {
    return <div />;
  }
}

const B = styled.div`
  ${A} {
  }
`;

这个例子是不可以的,因为A继承ReactComponent,不是被styled构造过的。我们的组件选择器只支持在Styled Components创建的样式组件。

class A extends React.Component {
  render() {
    return <div className={this.props.className} />;
  }
}

const StyledA = styled(A)``;

const B = styled.div`
  ${StyledA} {
  }
`;

API文档

基本

  • styled
  • .attrs
  • ``字符模板
  • ThemeProvider

助手

  • css
  • keyframes
  • injectGlobal
  • isStyledComponent
  • withTheme

支持CSS

在样式组件中,我们支持所有CSS加嵌套。因为我们生成一个真实的stylesheet而不是内联样式,所以CSS中的任何工作都在样式组件中工作!

(&)被我们所生成的、唯一的类名替换给样式组件,使其具有复杂的逻辑变得容易。

支持flow和typescript

更多工具

Babel Plugin

Test Utilities

Jest Styled Components,基于jest,可对styled-components做单元测试

demo

Stylelint

使用stylelint 检查我们的styled-components样式书写规范。

Styled Theming 语法高亮显示

在模板文本中写入CSS时丢失的一个东西是语法高亮显示。我们正在努力在所有编辑器中实现正确的语法高亮显示。支持大部分编辑器包括Visual Studio Code、WebStorm。

总结

下面简单总结一下 styled-components 在开发中的表现:

  • 提出了 container 和 components 的概念,移除了组件和样式之间的映射关系,符合关注度分离的模式;
  • 可以在样式定义中直接引用到 js 变量,共享变量,非常便利,利用js的特性为css附能,帅毙了!
  • 支持组件之间继承,方便代码复用,提升可维护性;
  • 兼容现有的 className 方式,升级无痛;
  • 这下写CSS也乐趣十足了。
  • styled-components的最基本思想就是通过移除样式和组件之间的映射来执行最佳实践
  • 一个让styled-components很容易被接受的特性:当他被怀疑的时候,你同样可以使用你熟悉的方法去使用它!

当然,styled-components 还有一些优秀的特性,比如服务端渲染和 React Native 的支持。



题外:styled-components的魔法

如果你从来没看见过styled-components,下面是一个简单的样式组件的例子:

const Button = styled.button`
  background-color: papayawhip;
  border-radius: 3px;
  color: palevioletred;
`

现在可以像使用普通React组件一样渲染使用。

<Button>Hi Dad!</Button>

那么,这是怎么工作的呢?这个过程中到底发生了什么魔法?

标签模板

实际上, style.button` `是JavaScript的新语法特性,属于ES6的标签模板功能。

本质上, styled.button` styled.button()`是一样的。他们的差异只在传递参数时就变得可见了。

styled-components利用模板字符串的用处在于可以给内部props赋值。

const Button = styled.button`
  font-size: ${props => props.primary ? '2em' : '1em'};
`
// font-size: 2em;
<Button primary />
查看原文

ziison 赞了文章 · 2019-06-02

从一道前端面试题谈起

今天在知乎上看到一个回答《为什么前端工程师那么难招?》,作者提到说有很多前端工程师甚至连单链表翻转都写不出来。说实话,来面试的孩子们本来就紧张,你要冷不丁问一句单链表翻转怎么写,估计很多人都会蒙掉。

clipboard.png

于是我在leetcode 上找了一下这道题,看看我能不能写得出来。

题目的要求很简单:

反转一个单链表。

示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

最后的解决就是这样的一行代码:

const reverseList = (head, q = null) => head !== null ? reverseList(head.next, { val: head.val, next: q }) : q;

答案并不重要,有意思的是整个的解题思路。

前端工程师需要了解算法吗?

在解题之前,我们先来聊聊算法。严格来说,单链表翻转这种问题只是对于链表这种数据结构的一种操控而已,根本谈不上是什么算法。当然,宽泛地来说,只要涉及到循环和递归的都把它归入到算法也可以。在这里,我们采用一种宽容的定义。

算法需要背吗?我觉得算法是不需要背的,你也不可能背的下来,光leetcode就有上千道题目,并且还在增加,怎么可能背的下来?所以对于现阶段的程序员来说,算法分为两类,一类是你自己能推算出来的,这种不用背,一类是你推算不出来的,比如KMP算法,这种也不用背,需要的时候直接Google就可以了。特别是对于前端以及80%的后端程序员来说,你需要什么算法,就直接使用现在的库就行了,数组排序直接array.sort就可以,谁没事还非要去写一个快速排序?

那为什么面试前端的时候还必须要考算法?这个道理基本上类似于通过考脑筋急转弯来测试智商一样,实际工作中是完全用不上的,就像高考的时候考一大堆物理、化学、生物,恨不得你上知天文,下知地理,上下五千年,精通多国语言,但其实你参加工作以后发现根本用不上一样,这其实就是一个智商筛子,过滤一下而已。

所以,别管工作中用不用得到,如果你想通过这道筛子的话,算法的东西多少还是应该学习一些的。

单链表的数据结构

说实话,我刚做这道题的时候,我也有点蒙。虽然上学的时候学过数据结构,链表、堆栈、二叉树这些东西,但这么多年实际工作中用的很少,几乎都快忘光了,不过没关系,我们就把它当成是脑筋急转弯来做一下好了。

我们先来看一下它的数据结构是什么样的:

var reverseList = function(head) {
    console.log(head);
};
ListNode {  
  val: 1, next: ListNode {
    val: 2, next: ListNode {
      val: 3, next: [ListNode] } } }

一个对象里包含了两个属性,一个属性是val,一个属性是next,这样一层一层循环嵌套下去。

通常来讲,在前端开发当中,我们最常用的是数组。如果是用数组的话,就太简单了,js数组自带reverse方法,直接array.reverse反转就行了。但是题目非要弄成链表的形式,说实在的,我真没有见过前端什么地方还需要用链表这种结构的(除了面试的时候),所以说这种题目对于实际工作是没什么用处的,但是脑筋急转弯的智商题既然这样出了,我们就来看看怎么解决它吧。

循环迭代

首先想到的,这肯定是一个while循环,循环到最后,发现nextnull就结束,这个很容易想。但关键是怎么倒序呢?这个地方需要稍微动一下脑子。我们观察一下,倒序之后的结果,1变成了最后一个,也就是说1nextnull,而2next1。所以我们一上来先构建一个nextnull1结点,然后读到2的时候,把2next指向1,这样不就倒过了吗?所以一开始的程序写出来是这样的:

var reverseList = function(head) {
  let p = head;
  let q = { val: p.val, next: null };
  while (p.next !== null) {
    p = p.next;
    q = { val: p.val, next: q };
  }
  return q;
};

先初始化了一个q,它的nextnull,所以它就是我们的尾结点,然后再一个一个指向它,这样整个链表就倒序翻转过来了。

第一个测试用例没有问题,于是就提交了,但是提交完了发现不对,如果head本身是null的话,会报错,所以修改了一下:

var reverseList = function(head) {
  let p = head;
  if (p === null) {
    return null;
  }
  let q = { val: p.val, next: null };
  while (p.next !== null) {
    p = p.next;
    q = { val: p.val, next: q };
  }
  return q;
};

这回就过了。

递归

解决是解决了,但是这么长的代码,明显不够优雅,我们尝试用递归的方法对它进一步优化。

如果有全局变量的话,递归本身并不复杂。但因为leetcode里不允许用全局变量,所以我们只好构造一个双参数的函数,把倒序之后的结果也作为一个参数传进去,这样刚一开始的时候q是一个null,随着递归的层层深入,q逐渐包裹起来,直到最后一层:

const reverseList = function(head) {
    let q = null;
    return r(head, q);
}
const r = function(p, q) {
    if (p === null) {
        return q;
    } else {
        return r(p.next, { val: p.val, next: q });
    }
}

这里我们终于理清了出题者的思路,用递归的方式我们可以把这个if判断作为整个递归结束的必要条件。如果p不是null,那么我们就再做一次,把p的下一个结点放进来,比如说1的下一个是2,那么我们这时候就从2开始执行,直到最后走到55的下一个结点是null,然后我们退回上一层,这样一层层钻下去,最后再一层层返回来,就完成了整个翻转的过程。

优化代码

递归成功之后,后面的事情就相对简单了。

怎么能把代码弄简短一些呢?我们注意到这里这个if语句里面都是直接return,那我们干脆直接做个三元操作符就好了:

const reverseList = function(head) {
    let q = null;
    return r(head, q);
}
const r = function(p, q) {
    return p === null ? q : r(p.next, { val: p.val, next: q });
}

更进一步,我们用箭头函数来表示:

const reverseList = (head) => {
    let q = null;
    return r(head, q);
}
const r = (p, q) => {
    return p === null ? q : r(p.next, { val: p.val, next: q });
}

箭头函数还有一个特色是如果你只有一条return语句的话,连外面的花括号和return关键字都可以省掉,于是就变成了这样:

const reverseList = (head) => {
    let q = null;
    return r(head, q);
}
const r = (p, q) => (p === null ? q : r(p.next, { val: p.val, next: q }));

这样是不是看着就短多了呢?但是还可以更进一步简化,我们把上面的函数再精简,这时候你仔细观察的话,会发现第一个函数和第二个函数很类似,都是在调用第二个函数,那么我们能不能精简一下把它们合并呢?我们先把第一个函数变换为和第二函数的参数数目一致的形式:

const reverseList = (head, q) => r(head, q);
const r = (p, q) => (p === null ? q : r(p.next, { val: p.val, next: q }));

但这时候出现了一个问题,如果q没有初始值的话,它是undefined,不是null,所以我们还需要给q一个初始值:

const reverseList = (head, q = null) => r(head, q);
const r = (p, q) => (p === null ? q : r(p.next, { val: p.val, next: q }));

这时候我们的两个函数长的基本一致了,我们来把它们合并一下:

const reverseList = (head, q = null) => (head === null ? q : reverseList(head.next, { val: head.val, next: q }));

看,这样你就得到了一个一行代码的递归函数可以解决单链表翻转的问题。

实话说,即使是像我这样有多年经验的程序员,要解决这样的一个问题,都需要这么长的时间这么多步骤才能优化完美,更何况说一个大学刚毕业的孩子,很难当场就一次性回答正确,能把思路说出来就很不容易了,但你可以从这个过程中看到程序代码是如何逐渐演进的。背诵算法没有意义,我觉得我们更多需要的是这一个思考的过程,毕竟编程是一个脑筋急转弯的过程,不是唐诗三百首。

查看原文

赞 51 收藏 26 评论 4

ziison 关注了专栏 · 2018-12-17

美团技术团队

“美团技术团队”,你最值得关注的技术团队官方微信公众号。 每周会推送来自一线的实践技术文章,涵盖前端(Web、iOS和Android)、后台、大数据、AI/算法、测试、运维等技术领域。 在这里,与8000多业界一流工程师交流切磋。

关注 12109

ziison 收藏了文章 · 2018-08-29

vue生命周期探究(二)

上一章我们介绍了vue的组件生命周期和路由勾子,这一章,让我们来看看在vue-cli项目中,各个勾子的顺序是如何的吧。主要聚焦在页面加载的这条时间线。

页面加载的时候,vue生命周期的触发顺序是怎样的呢?

那么进入某个路由对应的组件的时候,我们会触发哪些类型的周期呢?

  1. 根实例的加载相关的生命周期(beforeCreate、created、beforeMount、mounted)

  2. 组件实例的加载相关的生命周期(beforeCreate、created、beforeMount、mounted)

  3. 全局路由勾子(router.beforeEach)

  4. 组件路由勾子(beforeRouteEnter)

  5. 组件路由勾子的next里的回调(beforeRouteEnter)

  6. 指令的周期(bind,inserted)

  7. nextTick方法的回调

接下来,让我们用vue-cli简单改造后的项目,做一个测试,看看各个声明周期的触发顺序是怎样的

main.js:

router.beforeEach((to, from, next) => {
  console.log('路由全局勾子:beforeEach')
  next()
})

router.afterEach((to, from) => {
  console.log('路由全局勾子:afterEach')
})

new Vue({
  beforeCreate () {
    console.log('根组件:beforeCreate')
  },
  created () {
    console.log('根组件:created')
  },
  beforeMount () {
    console.log('根组件:beforeMount')
  },
  mounted () {
    console.log('根组件:mounted')
  }
  el: '#app',
  router,
  template: '<App/>',
  components: { App }
})

test.vue

<template>
  <h1 v-ooo @click = "$router.push('/')">test</h1>
</template>
<script>
export default {
  beforeRouteEnter (to, from, next) {
    console.log('组件路由勾子:beforeRouteEnter')
    next(vm => {
      console.log('组件路由勾子beforeRouteEnter的next')
    })
  },
  beforeCreate () {
    console.log('组件:beforeCreate')
  },
  created () {
    this.$nextTick(() => {
      console.log('nextTick')
    })
    console.log('组件:created')
  },
  beforeMount () {
    console.log('组件:beforeMount')
  },
  mounted () {
    console.log('组件:mounted')
  },
  directives: {
    ooo: {
      bind (el, binding, vnode) {
        console.log('指令binding')
      },
      inserted (el, binding, vnode) {
        console.log('指令inserted')
      }
    }
  }
}
</script>

接下来,直接进入test.vue对应的路由。在控制台,我们看到如下的输出

clipboard.png

我们看到执行的顺序为

  1. 路由勾子 (beforeEach、beforeRouteEnter、afterEach)

  2. 根组件 (beforeCreate、created、beforeMount)

  3. 组件 (beforeCreate、created、beforeMount)

  4. 指令 (bind、inserted)

  5. 组件 mounted

  6. 根组件 mounted

  7. beforeRouteEnter的next的回调

  8. nextTick

结论

路由勾子执行周期非常早,甚至在根实例的渲染之前

具体的顺序 router.beforeEach > beforeRouteEnter > router.afterEach

tip:在进行路由拦截的时候要避免使用实例内部的方法或属性。
在开发项目时候,我们脑门一拍把,具体拦截的程序,写在了根实例的方法上了,到beforeEach去调用。
结果导致整个拦截的周期,推迟到实例渲染的之后。
因此对于一些路由组件的beforeRouteEnter里的请求并无法拦截,页面看上去好像已经拦截下来了。
实际上请求依然发了出去,beforeRouteEnter内的函数依然执行了。

指令的绑定在组件mounted之前,组件的beforeMount之后

不得不提的, beforeRouteEnter的next勾子

beforeRouteEnter的执行顺序是如此靠前,而其中next的回调勾子的函数,执行则非常靠后,在mounted之后!!
我们通常是在beforeRouteEnter中加载一些首屏用数据,待数据收到后,再调用next勾子,通过回调的参数vm将数据绑定到实例上。
因此,请注意next的勾子是非常靠后的。

nextTick

越早注册的nextTick触发越早

vue生命周期探究(一)

查看原文

ziison 收藏了文章 · 2018-08-29

Vue2.0 探索之路——生命周期和钩子函数的一些理解

前言

在使用vue一个多礼拜后,感觉现在还停留在初级阶段,虽然知道怎么和后端做数据交互,但是对于mounted这个挂载还不是很清楚的。放大之,对vue的生命周期不甚了解。只知道简单的使用,而不知道为什么,这对后面的踩坑是相当不利的。

因为我们有时候会在几个钩子函数里做一些事情,什么时候做,在哪个函数里做,我们不清楚。

于是我开始先去搜索,发现vue2.0的生命周期没啥文章。大多是1.0的版本介绍。最后还是找到一篇不错的(会放在最后)

vue生命周期简介

clipboard.png

f847b38a-63fe-11e6-9c29-38e58d46f036.png

咱们从上图可以很明显的看出现在vue2.0都包括了哪些生命周期的函数了。

生命周期探究

对于执行顺序和什么时候执行,看上面两个图基本有个了解了。下面我们将结合代码去看看钩子函数的执行。

ps:下面代码可以直接复制出去执行
<!DOCTYPE html>
<html>
<head>
    <title></title>
    <script type="text/javascript" data-original="https://cdn.jsdelivr.net/vue/2.1.3/vue.js"></script>
</head>
<body>

<div id="app">
     <p>{{ message }}</p>
</div>

<script type="text/javascript">
    
  var app = new Vue({
      el: '#app',
      data: {
          message : "xuxiao is boy" 
      },
       beforeCreate: function () {
                console.group('beforeCreate 创建前状态===============》');
               console.log("%c%s", "color:red" , "el     : " + this.$el); //undefined
               console.log("%c%s", "color:red","data   : " + this.$data); //undefined 
               console.log("%c%s", "color:red","message: " + this.message)  
        },
        created: function () {
            console.group('created 创建完毕状态===============》');
            console.log("%c%s", "color:red","el     : " + this.$el); //undefined
               console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化 
               console.log("%c%s", "color:red","message: " + this.message); //已被初始化
        },
        beforeMount: function () {
            console.group('beforeMount 挂载前状态===============》');
            console.log("%c%s", "color:red","el     : " + (this.$el)); //已被初始化
            console.log(this.$el);
               console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化  
               console.log("%c%s", "color:red","message: " + this.message); //已被初始化  
        },
        mounted: function () {
            console.group('mounted 挂载结束状态===============》');
            console.log("%c%s", "color:red","el     : " + this.$el); //已被初始化
            console.log(this.$el);    
               console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化
               console.log("%c%s", "color:red","message: " + this.message); //已被初始化 
        },
        beforeUpdate: function () {
            console.group('beforeUpdate 更新前状态===============》');
            console.log("%c%s", "color:red","el     : " + this.$el);
            console.log(this.$el);   
               console.log("%c%s", "color:red","data   : " + this.$data); 
               console.log("%c%s", "color:red","message: " + this.message); 
        },
        updated: function () {
            console.group('updated 更新完成状态===============》');
            console.log("%c%s", "color:red","el     : " + this.$el);
            console.log(this.$el); 
               console.log("%c%s", "color:red","data   : " + this.$data); 
               console.log("%c%s", "color:red","message: " + this.message); 
        },
        beforeDestroy: function () {
            console.group('beforeDestroy 销毁前状态===============》');
            console.log("%c%s", "color:red","el     : " + this.$el);
            console.log(this.$el);    
               console.log("%c%s", "color:red","data   : " + this.$data); 
               console.log("%c%s", "color:red","message: " + this.message); 
        },
        destroyed: function () {
            console.group('destroyed 销毁完成状态===============》');
            console.log("%c%s", "color:red","el     : " + this.$el);
            console.log(this.$el);  
               console.log("%c%s", "color:red","data   : " + this.$data); 
               console.log("%c%s", "color:red","message: " + this.message)
        }
    })
</script>
</body>
</html>

create 和 mounted 相关

咱们在chrome浏览器里打开,F12console就能发现

beforecreated:el 和 data 并未初始化
created:完成了 data 数据的初始化,el没有
beforeMount:完成了 el 和 data 初始化
mounted :完成挂载

另外在标红处,我们能发现el还是 {{message}},这里就是应用的 Virtual DOM(虚拟Dom)技术,先把坑占住了。到后面mounted挂载的时候再把值渲染进去。

clipboard.png

update 相关

这里我们在 chrome console里执行以下命令

app.message= 'yes !! I do';

下面就能看到data里的值被修改后,将会触发update的操作。

clipboard.png

destroy 相关

有关于销毁,暂时还不是很清楚。我们在console里执行下命令对 vue实例进行销毁。销毁完成后,我们再重新改变message的值,vue不再对此动作进行响应了。但是原先生成的dom元素还存在,可以这么理解,执行了destroy操作,后续就不再受vue控制了。

app.$destroy();

clipboard.png

生命周期总结

这么多钩子函数,我们怎么用呢,我想大家可能有这样的疑问吧,我也有,哈哈哈。

beforecreate : 举个栗子:可以在这加个loading事件
created :在这结束loading,还做一些初始化,实现函数自执行
mounted : 在这发起后端请求,拿回数据,配合路由钩子做一些事情
beforeDestroy: 你确认删除XX吗? destroyed :当前组件已被删除,清空相关内容

当然,还有更多,继续探索中......

写在最后

本文是一个vue的生命周期的理解,如有错误还请大牛指正,让小子也有得进步。
如果对你有所帮助,那是我最大的荣幸。

对了,兄台,对你有帮助的话不妨点个收藏或者推荐再走。

3477288873-5808ad0a8d62c_articlex

参考文献

https://segmentfault.com/q/10...
http://www.cnblogs.com/gagag/...

感谢上面几位兄台的文章和提问。

另外新写了篇 vuex 的文章,欢迎各位给点意见。传送门:Vue2.0 探索之路——vuex入门教程和思考
新写了篇 vue-router的文章,也欢迎各位给点意见。传送门:Vue2.0 探索之路——vue-router入门教程和总结

最近更新文章:
Less 函数巧妙解决白天和夜间模式
Node环境变量 process.env 的那些事儿

查看原文

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-05-23
个人主页被 716 人浏览