4

一 目录

不折腾的前端,和咸鱼有什么区别

目录
一 目录
二 前言
三 知识点
四 前端历史演进
五 React 相比原生的好处
六 React 和 Vue 比对
6.1 相同之处
6.2 不同之处
七 React Fiber
八 React 生命周期
8.1 版本 之前
8.2 版本 之后
  8.2.1 挂载阶段
  8.2.2 更新阶段
  8.2.3 卸载阶段
九 setState
9.1 调用 setState 之后发生了什么?
9.2 setState 是同步还是异步?
十 React this 问题
十一 受控组件和非受控组件
十二 组件通讯
十三 Redux
十四 Mixin、HOC 和 Hook
14.1 Mixin
14.2 高阶组件(HOC)
14.3 Hook
十五 性能优化
十六 参看文献
16.1 面试知识点
16.2 系统
16.3 React 和 Vue 比对
16.4 生命周期
16.5 受控组件和非受控组件
16.6 Diff 和 虚拟 DOM
16.7 React 源码
16.8 React Mixin
16.9 React Hoc
16.10 React Hooks
16.11 React Fiber
16.12 服务端渲染(SSR)
16.13 性能优化
16.14 其他

二 前言

返回目录

React 是现如今流行的前端框架,也是很多大厂面试必备。

React 与 Vue 虽有不同,但同样作为一款 MV* 框架,虽然实现可能不一样,但在一些理念上还是有相似的,例如数据驱动、组件化、虚拟 DOM 等。

当然,还有一些问题,可能最近都没法搞清楚的了,毕竟罗马不是一天能建成的:

  • [ ] React 的实现原理?有什么优缺点?

这些往往要更深入挖掘方可得知自己的结论,任重道远,不停歇。

三 知识点

返回目录
  • [x] 前端历史演进
  • [x] React 和原生对比

    • [x] 组件化
    • [x] 天然分层
    • [x] 生态好
    • [x] 开发效率
  • [x] React 和 Vue 对比

    • [x] 相同之处:虚拟 DOM、组件化、构建工具、配套框架、Chrome 开发工具
    • [x] 不同之处:模板 和 JSX、监听数据变化方式不同、Diff 不同
  • [x] React Fiber
  • [x] React 生命周期

    • [x] 挂载阶段:constructorgetDerivedStateFromPropsrendercomponentDidMount
    • [x] 更新阶段:getDerivedStateFromPropsshouldComponentUpdaterendergetSnapshotBeforeUpdatecomponentDidUpdate
    • [x] 卸载阶段:componentWillUnmount
  • [x] setState

    • [x] 调用之后发生什么
    • [x] 同步还是异步
  • [x] this 指向问题

    • [x] 通过 bind 修正
    • [x] 通过箭头函数修正
    • [x] bind 和箭头函数的区别
  • [x] 受控组件和非受控组件:valuedefaultValue
  • [x] 组件通讯

    • [x] props
    • [x] Context
    • [x] Redux
  • [x] ReduxReduxReact-Redux 以及 Redux-Saga 工作流
  • [x] MixinHOCHook
  • [x] 性能优化

    • [x] 首屏渲染优化 prerender-spa-plugin
    • [x] 页面占位 react-placeholder
    • [x] 页面切换优化 html-webpack-plugin
    • [x] 减少业务体积代码 Tree Shaking
    • [x] 提取公共代码 SplitChunkPlugin
    • [x] 切分代码 Code Splitting
    • [x] 懒加载 react-lazyload

四 前端历史演进

返回目录
  • jQuery 时代

通过 Ajax 从后端获取数据,然后通过 jQuery 生成 DOM 结果更新到页面中。

但是随着业务发展,项目越来越复杂,交互性越来越强,往往用户在某个时刻可能操作好几块内容,从而 DOM 的操作越来越频繁,页面性能逐步降低,用户也不满意这样卡慢的现状了。

  • MVVM

这时候有了 MVVM,双向数据绑定让数据在修改的时候同步 DOM 的更新,反之亦可。

这个设定大大降低手动维护 DOM 的成本,而 MVVM 为 React 的特性之一,虽然 React 属于单项数据流,需要我们手动实现双向数据绑定。

  • 虚拟 DOM

光靠绑定是不够的,这样没法解决频繁操作 DOM 的问题。

所以 React 内部实现了一套虚拟 DOM 的更新,它将真实 DOM 在 JS 中做一套缓存,每次有数据更新的时候,先内部通过 Diff 算法进行比对,然后收集一箩筐更新后,才对 DOM 进行更新,这样就大大降低了 DOM 的操作次数。

  • Diff 运作

那么,Diff 怎么运作呢?

Diff 获取虚拟 DOM 节点变更的 4 种情况比较:节点类型变了、节点类型一样,仅仅属性或者属性值变了、文本变了、增加、删除或者移动了子节点。

  • setState

React 不同于 Vue,可以通过 v-model 的形式,让用户的操作和 JavaScript 存储的数据同步更新,它需要通过 setState 来更新组件内容。

  • Redux

但是,如果想通过一个组件来渲染它兄弟组件,React 一开始在这块做得并不是那么好,所以就需要引入一个状态管理中心,来帮助我们管理状态(state),因而就有了 Redux

Redux 中,当 state 有变化的时候,依赖这个 state 的组件就会重新渲染,这样就解决了组件间数据传递的问题。

  • Mobx

Redux 有个问题,就是修改某个 state 的时候,需要经过 action.jstypes.jsreducers.js 这一系列文件,这样子 Redux 的数据流虽然非常正规,但是写起来复杂啊。

所以,社区又出现了另一套解决方案,也就是 Mobx

Mobx 推崇代码简约移动,只需要定义一个可贯彻的对象,然后在哪个组件中使用到了这个可观察对象,并且这个对象的数据有更改,那就会重新渲染,这使得 Mobx 开发项目的时候可以简单快速地完成很多功能。

但是 Mobx 也有缺点,就是数据流太过随意,出了 Bug 不好定位。

  • End

所以,针对于小项目来说,社区推荐使用 MobX,对大项目推荐使用 Redux。

本段文本参考的内容已记录在参考文献中

五 React 相比原生的好处

返回目录
  • 组件化: 其中以 React 的组件化最为彻底,甚至可以到函数级别的原子组件,高度的组件化可以是我们的工程易于维护、易于组合拓展。
  • 天然分层: JQuery 时代的代码大部分情况下是面条代码,耦合严重,现代框架不管是 MVC、MVP 还是 MVVM 模式都能帮助我们进行分层,代码解耦更易于读写。
  • 生态: 现在主流前端框架都自带生态,不管是数据流管理架构还是 UI 库都有成熟的解决方案。
  • 开发效率: 现代前端框架都默认自动更新 DOM,而非我们手动操作,解放了开发者的手动 DOM 成本,提高开发效率,从根本上解决了 UI 与状态同步问题.

六 React 和 Vue 比对

返回目录

6.1 相同之处

返回目录
  1. 虚拟 DOM。映射真实 DOM,通过新旧 DOM 的 diff 对比,更好的跟踪渲染页面。
  2. 组件化。将应用拆分成一个个功能明确的模块,每个模块之间可以通过合适的方式互相联系。
  3. 构建工具。都有自己的构建工具,通过 Webpack + Babel 去搭建脚手架。
  4. Chrome 开发工具。两者都有很好的 Chrome 扩展去帮助查找 Bug。
  5. 配套框架。Vue 有 Vue-routerVuex,而 React 有 React-routerReact-Redux

6.2 不同之处

返回目录
  1. 模板 VS JSX。Vue 推荐编写近似常规 HTML 的模板进行渲染,而 React 推荐 JSX 的书写方式。
  2. 监听数据变化的不同。Vue 使用的是可变数据,而 React 更强调数据的不可变。在 Vue 中通过 v-model 绑定的数据,用户改变输入值后对应的值也相应改变。而 React 需要通过 setState 进行设置变化。
  3. Diff 不同。Vue 通过双向链表实现边对比边更新 DOM,而 React 通过 Diff 队列保存需要更新的 DOM,得到 patch 树,再统一批量更新 DOM。
  4. 开发团队。Vue 一开始核心就是 Evan You,后面再招了其他人组成团队;React 的话是一开始就是 Facebook 团队搞的。所以网上的人比对源码情况的话,Vue 的比 React 的简单易懂点。
实话实说是看别人说的异同,担心有 “小伙伴” 有自己观点,然后在这里被喷,jsliang 不敢持有任何自己的观点,但是面试我总是要这么回答面试官的 /滑稽

七 React Fiber

返回目录

React 的核心流程可以分为两个部分:

  • reconciliation (调度算法,也可称为 render):

    • 更新 stateprops
    • 调用生命周期钩子
    • 生成虚拟 DOM
    • 通过新旧 VDOM 进行 diff 算法,获取 VDOM change
    • 确定是否需要重新渲染
  • commit

    • 如需要,则操作 DOM 节点更新;

为什么需要 Fiber

随着应用变得越来越庞大,整个更新渲染的过程开始变得吃力,大量的组件渲染会导致主进程长时间被占用,导致一些动画或高频操作出现卡顿和掉帧的情况。

而关键点,便是 同步阻塞

在之前的调度算法中,React 需要实例化每个类组件,生成一颗组件树,使用 同步递归 的方式进行遍历渲染,而这个过程最大的问题就是无法 暂停和恢复

所以,为了解决这个问题(同步阻塞),通常有两种方法: 异步任务分割

React Fiber 便是为了实现任务分割而诞生的。

React Fiber 简述:

  • 在 React 16 版本中将调度算法进行了重构, 将之前的 stack reconciler 重构成新版的 fiber reconciler,变成了具有链表和指针的 单链表树遍历算法。通过指针映射,每个单元都记录着遍历当下的上一步与下一步,从而使遍历变得可以被暂停和重启。
  • 可以理解为是一种 任务分割调度算法,主要是将原先同步更新渲染的任务分割成一个个独立的 小任务单位,根据不同的优先级,将小任务分散到浏览器的空闲时间执行,充分利用主进程的事件循环机制。

React Fiber 核心:

  • 可以具象为一个数据结构
  • 链表树遍历算法
  • 任务分割
  • 分散执行
  • 优先级策略

八 React 生命周期

返回目录

React 逐渐废弃的生命周期方法:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

8.1 版本 之前

返回目录

8.2 版本 之后

返回目录

8.2.1 挂载阶段

返回目录
  • constructor:构造函数,最先被执行,通常在构造函数中初始化 state 对象或者给自定义方法绑定 this
  • getDerivedStateFromPropsstatic getDerivedStateFromProps(nextProps, prevState),这是个静态方法,当我们接收到新的属性想去修改我们 state,可以使用 getDerivedStateFromProps
  • renderrender 函数是个纯函数,只返回需要渲染的东西,不应该包含其他的业务逻辑,可以返回原生 DOM、React 组件、FragmentPortals、字符串和数字等内容。
  • componentDidMount:组件装载之后调用,此时我们可以获取到 DOM 节点并操作,比如对 CanvasSVG 等操作。服务器请求、订阅都可以写这个里面,但是记得在 componentWillUnmount 中取消订阅。

React 的接口请求是放在 componentDidMount 里面比较合适,旧版本有人放在 componentWillMount 里面,从而导致多次请求,现在 componentWillMount 不推荐使用了,所以转 componentDidMount 就非常科学了。

存在以下问题:

  • 为什么 getDerivedStateFromProps 是静态的?

当它设置为静态函数,表明这个函数不能通过 this 访问到 class 的属性,也并不推荐直接访问属性。

  • 哪些生命周期可以 setState

可以在 componentDidMountcomponentDidUpdate 中使用,此时 DOM 已经稳定下来了,可以进行数据的操作了。

8.2.2 更新阶段

返回目录
  • getDerivedStateFromProps:此方法在更新阶段也会被调用。
  • shouldComponentUpdateshouldComponentUpdate(nextProps, nextState),有两个参数,表示新的属性和变化之后的 state,返回一个布尔值。如果是 true 表示会触发重新渲染,false 表示不会触发重新渲染,默认返回 true。可以利用这个生命周期来优化 React 程序性能。
  • render:同挂载阶段 render
  • getSnapshotBeforeUpdategetSnapshotBeforeUpdate(prevProps, prevState),这个方法会在 render 之后,componentDidUpdate 之前调用,有两个参数,表示之前属性和之前的 state。这个函数有一个返回值,会作为第三个参数传给 componentDidUpdate,如果不需要返回值,可以返回 null,这个方法必须和 componentDidUpdate 配合使用。
  • componentDidUpdatecomponentDidUpdate(prevProps, prevState, snapshot),在 getSnapshotBeforeUpdate
    之后调用,有三个参数,表示之前的 props,之前的 state,以及 snapshot。参数 snapshotgetSnapshotBeforeUpdate 返回的,如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或者计算过程迁移到 getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统一触发回调或者更新状态。

8.2.3 卸载阶段

返回目录
  • componentWillUnmount:当组件被卸载或者销毁时会被调用,在这里清除定时器,或者取消网络请求,用来清理无效的 DOM 元素等垃圾回收工作。

九 setState

返回目录

setState 是 React 中用于修改状态,更新视图的方法。

9.1 调用 setState 之后发生了什么?

返回目录

在代码中调用 setState 之后,React 会将传入的参数对象与组件当前的状态合并,触发所谓的调和过程(Reconciliation)。

经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。

在 React 得到元素树之后,React 会自动计算新树和老树之间的节点差异,然后根据差异对界面进行最小化重新渲染。

在差异计算算法(Diff)中,React 能够相对精确地知道哪些位置发生了改变以及英国如何改变,保证了按需更新,而不是全部重新渲染。

简单来说:

  1. 合并参数对象,触发调和过程
  2. 计算新树和老树差异(Diff
  3. 根据差异进行最小化重新渲染

9.2 setState 是同步还是异步?

返回目录

回答:有时候同步,有时候异步。

  1. setState 在合成事件和钩子函数中是异步的,在原生事件和 setTimeout 是同步的。
  2. setState 的异步,并不是说内部由异步代码实现,它本身执行的过程和代码是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,从而形成了所谓的异步。
  3. setState 可以通过第二个参数 setState(partialState, callback),在回调方法中拿到更新后的结果。

十 React this 问题

返回目录

在 React 中有几种方法可以修正 this 的指向,这里例举 4 种方法:

import React, { Component } from 'react'

class App extends Component {
  constructor (props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick () {
    console.log('jsliang 2020');
  }
  handleClick2 = () => {
    console.log('jsliang 2021');
  }
  render () {
    // 四种绑定方法
    return (
      <div className='App'>
        {/* 方法一:通过 constructor 中进行 bind 绑定 */}
        <button onClick={this.handleClick}>btn 1</button>

        {/* 方法二:在里边绑定 this */}
        <button onClick={this.handleClick.bind(this)}>btn 2</button>

        {/* 方法三:通过箭头函数返回事件 */}
        <button onClick={() => this.handleClick()}>btn 3</button>
        
        {/* 方法四:让方法变成箭头函数 */}
        <button onClick={this.handleClick2}>btn 4</button>
        
        {/* 额外:直接调用不需要绑定 this */}
        {this.handleClick()}
      </div>
    )
  }
}

export default App;

那么,使用 bind 和箭头函数有什么区别吗?

箭头函数除了代码少,与普通函数最大的不同就是:this 是由声明该函数时候定义的,一般是隐性定义为声明该函数时的作用域 this

通过 bind 的话,相当于:Foo.prototype.a = function() {},是通过原型链的一个指正绑定。

而通过箭头函数的话,就相当于:

class Foo {
  constructor() {
    this.a = () => {};
  }
}

十一 受控组件和非受控组件

返回目录

Ant Design 中,对 Input 输入框进行操作,如果是改变 defaultValue 会发现毫无作用。

这是因为 React 的 form 表单组件中的 defaultValue 一经传递值后,后续改变 defaultValue 都将不起作用,被忽略了。

具体来说这是一种 React 非受控组件,其状态是在 input 的 React 内部控制,不受调用者控制。

所以受控组件就是可以被 React 状态控制的组件。双向数据绑定就是受控组件,你可以为 form 中某个输入框添加 value 属性,然后控制它的一个改变。而非受控组件就是没有添加 value 属性的组件,你并不能对它的固定值进行操作。

十二 组件通讯

返回目录
  • 父组件向子组件通讯:父组件向子组件传 props 方式,向子组件进行通讯。
  • 子组件向父组件通讯:父组件在 props 中传递方法,然后子组件调用这个方法,将自身需要传递的信息,传递到父组件的作用域中。
  • 复杂点的通讯:借助 React 的 Context,或者 Redux 进行数据通讯。

十三 Redux

返回目录

网上有挺多关于 ReduxReact-ReduxRedux-Saga 的使用,这里就不废话介绍了,还是讲讲 jsliang 在工作中的一个使用吧。

工作目录
- 某个页面文件夹
   - View.jsx     当前页面主入口
   - Child.jsx    子组件
   - Brother.jsx  兄弟组件
   - action.js    动作
   - types.js     类型
   - saga.js      调用接口
   - reducers.js  处理数据

正常的一个工作目录如上所示,我们工作中是怎么个使用方式呢?

首先,在 View.jsx 中通过 React-Redux 连接 ReactRedux

然后,假设现在 Child.jsx 需要调用接口(异步处理),那么会:

  1. action.js 中定义这个方法,会传递什么参数。
  2. 其中 types.js 是辅助 action.js 的一个内容,为了防止方法体的重复,我们会在 types.js 中定义大写的 action 名字。
  3. 这时候就可以在 View.jsx 中通过 dispatch 触发方法,例如 dispatch(getPage(page, perPage))
  4. 这时候,在 reducers.js 中和 sage.js 中都能监听到这个方法,但是我们是在 sage.js 中调用接口并处理数据。
  5. 处理完毕之后,再将 sage.js 中的传递给 reducers.js 中,让它去处理数据。

接着,如果 Brother.jsx 只是单纯地想处理数据并在 Child.jsx 中使用,那么我们处理方式是跟上面一样的,只是直接在 reducers.js 中处理,而不需要再在 sage.js 中调用接口而已。

最后,我们再看看 reduxreact-reduxt 的工作流程加深印象:

Redux

React-Redux

十四 Mixin、HOC 和 Hook

返回目录

前端发展速度非常之快,页面和组件变得越来越复杂,如何更好的实现 状态逻辑复用 一直都是应用程序中重要的一部分,这直接关系着应用程序的质量以及维护的难易程度。

MixinHOCHook 是 React 采用的 3 种 状态逻辑复用 的技术,Mixin 已被抛弃,HOC 正当壮年,Hook 初露锋芒,掌握它迭代因素和规律非常重要。

14.1 Mixin

返回目录

Mixin(混入)是一种通过扩展收集功能的方式,它本质上是将一个对象的属性拷贝到另一个对象上面去。

不过你可以拷贝任意多个对象的任意个方法到一个新对象上去,这是继承所不能实现的。

它的出现主要就是为了解决代码复用问题。

但是,它会带来一些危害:

  • Mixin 相互依赖、相互耦合,不利于代码维护
  • 不同的 Mixin 中的方法可能会互相冲突
  • Mixin 非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样给代码造成滚雪球式的复杂性。

14.2 高阶组件(HOC)

返回目录

基于 Mixin 的问题,React 推出对装饰模式的一种实现:高阶组件(HOC)。

高阶组件接收一个组件作为参数,并返回一个新的组件。

高阶组件(HOC)是 React 中的高级技术,用来重用组件逻辑。但高阶组件本身并不是 React API。它只是一种模式,这种模式是由 React 自身的组合性质必然产生的。
function visible(WrappedComponent) {
  return class extends Component {
    render() {
      const { visible, ...props } = this.props;
      if (visible === false) return null;
      return <WrappedComponent {...props} />;
    }
  }
}

高阶组件可以应用于 日志打点、可用权限控制、双向绑定、表单校验等。

高阶组件解决了 Mixin 带来的问题:

  • 低耦合。高阶组件就是一个没有副作用的纯函数,各个高阶组件不会互相依赖耦合
  • 避免冲突。高阶组件也有可能造成冲突,但我们可以在遵守约定的情况下避免这些行为
  • 副作用小。高阶组件并不关心数据使用的方式和原因,而被包裹的组件也不关心数据来自何处。高阶组件的增加不会为原组件增加负担

但是,有光的地方总有暗,高阶组件也存在一些缺陷:

  • HOC 需要在原组件上进行包裹或者嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这让调试变得非常困难。
  • HOC 可以劫持 props,在不遵守约定的情况下也可能造成冲突。

14.3 Hook

返回目录

Hook 是 React v16.7.0-alpha 中加入的新特性。它可以让你在 class 以外使用 state 和其他 React 特性。

使用 Hook,你可以在将含有 state 的逻辑从组件中抽象出来,这将可以让这些逻辑容易被测试。

同时,Hook 可以帮助你在不重写组件结构的情况下复用这些逻辑。

所以,它也可以作为一种实现状态逻辑复用的方案。

Hook 使用带来的好处:

  • 减少状态逻辑复用的风险HookMixin 在用法上有一定的相似之处,但是 Mixin 引入的逻辑和状态是可以相互覆盖的,而多个 Hook 之间互不影响,这让我们不需要在把一部分精力放在防止避免逻辑复用的冲突上。
  • 避免地狱式嵌套:大量使用 HOC 的情况下让我们的代码变得嵌套层级非常深,使用 Hook,我们可以实现扁平式的状态逻辑复用,而避免了大量的组件嵌套。
  • 让组件更容易理解:在使用 class 组件构建我们的程序时,他们各自拥有自己的状态,业务逻辑的复杂使这些组件变得越来越庞大,各个生命周期中会调用越来越多的逻辑,越来越难以维护。使用 Hook,可以让你更大限度的将公用逻辑抽离,将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割。
  • 使用函数代替 class:相比函数,编写一个 class 可能需要掌握更多的知识,需要注意的点也越多,比如 this 指向、绑定事件等等。另外,计算机理解一个函数比理解一个 class 更快。Hooks 让你可以在 class 之外使用更多 React 的新特性。

十五 性能优化

返回目录
  1. 首屏渲染优化<div id="root"> SVG </div>,也可以使用插件 prerender-spa-plugin 插件进行首屏渲染。
  2. 页面切换优化。使用 html-webpack-plugin 插件自动插入 loading,这样切换的时候,就不需要在每个页面都写一套 loading
  3. 减少业务代码体积。通过 Tree Shaking 来减少一些代码。
  4. 提取公共代码。通过 SplitChunkPlugin 自动拆分业务基础库,减少大文件的存在。
  5. 切分代码。通过 Code Splitting 来懒加载代码,提高用户的加载体验。例如通过 React Loadable 来将组件改写成支持动态 import 的形式。
  6. 懒加载。React 可以通过 react-lazyload 这种成熟组件来进行懒加载的支持。
  7. 页面占位。有时候加载页面的文本、图片的时候,会出现 “闪屏” 的情况,比如图片或者文字没有加载完毕,对应位置空白,然后加载完毕,会突然撑开页面,导致闪屏。这时候使用第三方组件 react-placeholder 可以解决这种情况。

十六 参看文献

返回目录

本系列有 67 篇参考文献。

16.1 面试知识点

返回目录

2019

2018:

2017

16.2 系统

返回目录

2017

16.3 React 和 Vue 比对

返回目录

2020

2017

16.4 生命周期

返回目录

最新

2019

2018

2017

16.5 受控组件和非受控组件

返回目录

2019

2016

16.6 Diff 和 虚拟 DOM

返回目录

2019

2018

2017

2015

16.7 React 源码

返回目录

2018

2017

2015

16.8 React Mixin

返回目录

2015

16.9 React Hoc

返回目录

2019

2017

16.10 React Hooks

返回目录

2019

2018

16.11 React Fiber

返回目录

2018

16.12 服务端渲染(SSR)

返回目录

16.13 性能优化

返回目录

2019

2018

2017

16.14 其他

返回目录

2019

2018

2017

2015


jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。<br/>基于 https://github.com/LiangJunrong/document-library 上的作品创作。<br/>本许可协议授权之外的使用权限可以从 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 处获得。

jsliang
393 声望31 粉丝

一个充满探索欲,喜欢折腾,乐于扩展自己知识面的终身学习斜杠程序员