RainyCG

RainyCG 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 rain120.github.io/study-notes/ 编辑
编辑

18应届,前端菜鸟

个人动态

RainyCG 发布了文章 · 2019-11-04

这些前端资源,你值得拥有

<!--

笔记地址

因为之前学习前端的收集书签的资源太多,所以就在今年3、4月开始抽空整理了所有的书签资源,并归类,现在分享给大家,欢迎给我 提 issue or PR

Github, 知乎,掘金

因为图片太多,所以整理出来一版无图的,如果你觉得有图更好,欢迎跳转带图片的版本

DevDocs: API Documentation Browser -> Github

框架

React
React是一个为数据提供渲染为HTML视图的开源JavaScript 库。React视图通常采用包含以自定义HTML标记规定的其他组件的组件渲染。React为程序员提供了一种子组件不能直接影响外层组件的模型,数据改变时对HTML文档的有效更新,和现代单页应用中组件之间干净的分离。

React - Antd: antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。

UxCore: 为企业级后台而生的PC组件库。

ZanUI: PC、移动、小程序

React.part: 查找React的组件

Vue
Vue.js是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的Web应用框架。 2016年一项针对JavaScript的调查表明,Vue有着89%的开发者满意度。 在GitHub上,该项目平均每天能收获95颗星,为Github有史以来星标数第3多的项目。

Vue - Antd

IView: 一套基于 Vue.js 的高质量

Element: Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库

Mint UI: 基于 Vue.js 的移动端组件库

VUX: 一个凑合的 Vue.js 移动端 UI 组件库

Vue-Map: 基于 Vue 2.x 和高德地图的地图组件, Vue-Map文档

Nodejs

Node.js是一个能够在服务器端运行JavaScript的开放源代码、跨平台JavaScript运行环境。Node.js由Node.js基金会持有和维护,并与Linux基金会有合作关系。

Express: 高度包容、快速而极简的 Node.js Web 框架

koa

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

egg: 为企业级框架和应用而生

Nodejs学习笔记

Javascript

现代 Javascript 教程: 从基础知识到高阶主题,只需既简单又详细的解释。

Philip Roberts: Javascript可视化运行 -> Github -> Demo

Lodash: Lodash是一个JavaScript库,它使用函数式编程范例为常见的编程任务提供实用程序功能。

Ramda: 一款实用的JavaScript 函数式编程库。

Underscore : -> Github: Underscore.js是一个JavaScript库,为常见的编程任务提供实用程序功能。它与Prototype.js和Ruby语言提供的功能相当,但选择功能性编程设计而不是扩展对象原型。

30 seconds of code -> Github: 精心收集的有用的 JavaScript 代码片段,可以让你在 30 秒或更少的时间内理解

AST Explorer: 用于探索各种解析器生成的AST的Web工具。

JavaScript 版本兼容性查询

前端代码片段

flaviocopes: 一些JavaScript编程教程

Web 相关

Can I Use: 查询浏览器的特性支持情况

Package Different

查询 NodeJS 的 ES2018 特性支持情况

Web Dev: Google官方Web开发者资源

Mock

Easy-Mock: 高效伪造数据

Mock.js:生成随机数据,拦截 Ajax 请求

Rapid-Api: 构建块来增强您的应用程序, 发现并连接世界上最大的API中心的数千个API.

动画库(Javascript & CSS)

Threejs -> Github

Three.js是一个跨浏览器的脚本,使用JavaScript函数库或API来在网页浏览器中创建和展示动画的三维计算机图形。Three.js使用WebGL。源代码托管在GitHub

Animate.css: 一个使用CSS3的animation制作的动画效果的CSS集合,里面预设了很多种常用的动画,且使用非常简单。

Animejs: Anime.js是一个轻量级JavaScript动画库,具有简单但功能强大的API。它适用于CSS属性,SVG,DOM属性和JavaScript对象。

TweenMax.js

适用于移动端和现代互联网的超高性能专业级动画插件。

Tweenmax是GreenSock 动画平台的核心,配合其他插件 可动画CSS属性、滤镜效果、 颜色、 声音、 色彩、 帧、 饱和度、 对比度、 色调、 色系、 亮度、 贝塞尔

GreenSock: 适用于现代网络的超高性能专业级动画

Notes: 11 JavaScript Animation Libraries For 2019

Kubt.js -> Github: KUTE.js是一个Javascript动画引擎,具有顶级性能,内存效率和模块化代码。 它提供了大量工具来帮助您创建出色的自定义动画。

Hover -> Github: CSS3动力悬停效果的集合,适用于链接,按钮,徽标,SVG,特色图像等。 轻松应用于您自己的元素,修改或仅用于灵感。 提供CSS,Sass和LESS。

css-tricks: 分享使用CSS样式的技巧、经验和教程等。值得前端开发者阅读收藏的国外网站。

V8 引擎: 了解支持 Chrome 和 NodeJS 的 Google 开源高性能 Javascript 和 WebAssembly 引擎

测试框架

Mocha -> Mocha GitHub: MochaNode.js程序的JavaScript测试框架,具有浏览器支持,异步测试,测试覆盖率报告以及任何断言库的使用。

Chai: Chai是节点和浏览器的BDD / TDD断言库,可以与任何javascript测试框架愉快地配对。

Jest -> Jest Github: Jest是一个令人愉快的JavaScript测试框架,专注于简单性。它适用于以下项目:Babel,TypeScript,Node,React,Angular,Vue等等!

Karma: 一个 runner , 旨在帮助开发者简单而又快速的进行自动化单元测试 -> Github: karma测试框架的前世今生

Tape Github

jsPerf — JavaScript performance playground -> GitHub

优秀项目 & 插件

Webpack

Webpack config tool: webpack 配置工具

BootCDN: 稳定、快速、免费的前端开源项目 CDN 加速服务

BootStrap

BootsWatch: Free themes for Bootstrap

RxJS: 使用 Observables 的响应式编程的库,它使编写异步或基于回调的代码更容易。-> Github -> 中文文档

Layui -> Github

Awesome Lists

图表库

Echarts -> Github

AntV -> Github

G2

G2 是一套基于可视化编码的图形语法,以数据驱动,具有高度的易用性和扩展性,用户无需关注各种繁琐的实现细节,一条语句即可构建出各种各样的可交互的统计图表。

G6

G6 是一个简单、易用、完备的图可视化引擎,它在高定制能力的基础上,提供了一系列设计优雅、便于使用的图可视化解决方案。能帮助开发者搭建属于自己的图 图分析 应用或是 图编辑器 应用。

F2

F2 是一个专注于移动,开箱即用的可视化解决方案,完美支持 H5 环境同时兼容多种环境(Node, 小程序,Weex),完备的图形语法理论,满足你的各种可视化需求,专业的移动设计指引为你带来最佳的移动端图表体验。

L7

L7 中的 L 代表 Location,7 代表世界七大洲,寓意能为全球位置数据提供可视化能力。L7 的目标是提供一套地理空间数据可视化框架,易用易扩展,支持海量数据的高性能和 3D 高质量渲染,安全可靠(无地图法务风险)的地理空间数据可视化解决方案。

Recharts -> Github

Datamatic

开发资源

Awesomes前端开发资源

语雀 IT 百科精品知识库

算法学习 & 机器学习

Rappid算法学习

AI算法工程师手册

机器深度学习

VisuAlgo - 数据结构和算法动态可视化 (Chinese)

Algorithm Visualizer -> Github

Papers With Code : the latest in machine learning

Data Structure Visualizations: 旧金山大学CS Data Structure

BestofML: 收集汇总了机器学习相关的资源,包括书籍、课程、博客、论文等 -> Github

internetfundamentals:了解Web的工作原理,并迈出创建网页的第一步! 一个完全免费的视频课程,适合初学者

数学知识学习

微积分

线性代数

概率论

最优化方法

Math ∩ Programming

Immersive Linear Algebra: 一本会动的线代书,O(∩_∩)O哈哈~

机器学习的数学基础知识 ->Github -> Download

机器学习 - The Hundred-Page Machine Learning Book

MIT 的数据结构课程

计算机科学速成课(视频): Youtube 热门视频课程

Linux

Linux命令大全

Explain Shell -> Github: 一个解释 shell 的网站,你不理解某个命令的时候,在网站输入这个命令,网站会自动帮你分解解释对应参数的意思。

Iodide: Mozilla 支持的在 Web 中实现各种数据科学的效果 -> Github

Icon & 设计 & 网页

Iconfont

FontAwesome

Ionicons

Icomoon

Mobiriseicons

zwicon

unDraw

优设

uiforus

它免费提供设计素材、插件、工具,其中设计素材包括:UI 、图标、网页、插画、实物、桌面、组件、表单、字体,并且按照不同的设计软件对应的文件进行分类,包括:Photoshop 、Sketch 、Adobe Xd 、Illustrator 文件。

Product Hunt: 产品相关

Issue Hunt: issue需要帮助或者有余力去帮助别人,当然也可以赚钱哦

网络安全的教程

以一个黑客的角度将你带入,它配套了十几个小demo,一步一步带你发现各种各样的安全漏洞。因为这些demo都是交互式,玩起来很带感。

开发社区 & 学习社区

Medium: 轻量级内容发行的平台,允许单一用户或多人协作,将自己创作的内容以主题的形式结集为专辑(Collection),分享给用户进行消费和阅读

Stack Overflow: 最好的软件类问答网站了,给软件开发人员工作和学习提供了非常大的便利

Vue.js 社区

React.js社区

掘金

InfoQ: InfoQ 是一个实践驱动的社区资讯站点,致力于促进软件开发领域知识与创新的传播。

w3cplus

V2EX

大前端

开源中国

segmentfault

best-chinese-front-end-blogs: 收集优质的中文前端博客

softnshare

路径及文章

众成翻译

Fly63前端

博客 & 团队

阮一峰ES6入门

廖雪峰官网

AlloyTeam - 腾讯Web前端团队

IMWeb前端社区

凹凸实验室

淘宝前端团队FED

奇舞团

腾讯互娱

FEX 百度前端研发部

JDC 京东设计中心

携程UED

饿了么前端

美团前端

路径

程序员不能错过的28份技术知识图谱,你的进阶路上必备

学好机器学习需要哪些数学知识?

浏览器的工作原理

工具

Repl.it: 不要浪费时间建立一个开发环境。它为您提供了一个即时的IDE,让您可以在一个地方学习、构建、协作和托管所有内容。

Glitch

正则表达式可视化工具

Regulex

Regulex是一个JavaScript正则表达式可视化工具,由纯JavaScript实现,源码托管在Github上。

Regexper

Regexper是由Jeff Avallone开发的一款JavaScript正则表达式可视化工具,源码托管在Github上。它能够让正则表达式字符串以 Railroad 形式图形化,便于阅读和理解。同时推荐一款 JavaScript 正则在线测试工具——Regexpal,可以和 Regexper 配合使用。

Debuggex

Debuggex是一个测试正则表达式的Web应用,它支持JavaScriptPython以及PCRE

国际化应用的利器: 发现一个制作国际化应用的利器。该网站收集各种语言包,你输入中文,它返回各种语言包对这个词的翻译。

前端网站导航

D2日报

心谭博客

查看原文

赞 87 收藏 78 评论 0

RainyCG 赞了文章 · 2019-06-10

30分钟精通React今年最劲爆的新特性——React Hooks

你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?
——拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function。

你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?
——拥有了Hooks,生命周期钩子函数可以先丢一边了。

你在还在为组件中的this指向而晕头转向吗?
——既然Class都丢掉了,哪里还有this?你的人生第一次不再需要面对this。

这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张。如果你也对react感兴趣,或者正在使用react进行项目开发,答应我,请一定抽出至少30分钟的时间来阅读本文好吗?所有你需要了解的React Hooks的知识点,本文都涉及到了,相信完整读完后你一定会有所收获。

一个最简单的Hooks

首先让我们看一下一个简单的有状态组件:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

我们再来看一下使用hooks后的版本:

import { useState } from 'react';

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

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

是不是简单多了!可以看到,Example变成了一个函数,但这个函数却有自己的状态(count),同时它还可以更新自己的状态(setCount)。这个函数之所以这么了不得,就是因为它注入了一个hook--useState,就是这个hook让我们的函数变成了一个有状态的函数。

除了useState这个hook外,还有很多别的hook,比如useEffect提供了类似于componentDidMount等生命周期钩子的功能,useContext提供了上下文(context)的功能等等。

Hooks本质上就是一类特殊的函数,它们可以为你的函数型组件(function component)注入一些特殊的功能。咦?这听起来有点像被诟病的Mixins啊?难道是Mixins要在react中死灰复燃了吗?当然不会了,等会我们再来谈两者的区别。总而言之,这些hooks的目标就是让你不再写class,让function一统江湖。

React为什么要搞一个Hooks?

想要复用一个有状态的组件太麻烦了!

我们都知道react都核心思想就是,将一个页面拆成一堆独立的,可复用的组件,并且用自上而下的单向数据流的形式将这些组件串联起来。但假如你在大型的工作项目中用react,你会发现你的项目中实际上很多react组件冗长且难以复用。尤其是那些写成class的组件,它们本身包含了状态(state),所以复用这类组件就变得很麻烦。

那之前,官方推荐怎么解决这个问题呢?答案是:渲染属性(Render Props)高阶组件(Higher-Order Components)。我们可以稍微跑下题简单看一下这两种模式。

渲染属性指的是使用一个值为函数的prop来传递需要动态渲染的nodes或组件。如下面的代码可以看到我们的DataProvider组件包含了所有跟状态相关的代码,而Cat组件则可以是一个单纯的展示型组件,这样一来DataProvider就可以单独复用了。

import Cat from 'components/cat'
class DataProvider extends React.Component {
  constructor(props) {
    super(props);
    this.state = { target: 'Zac' };
  }

  render() {
    return (
      <div>
        {this.props.render(this.state)}
      </div>
    )
  }
}

<DataProvider render={data => (
  <Cat target={data.target} />
)}/>

虽然这个模式叫Render Props,但不是说非用一个叫render的props不可,习惯上大家更常写成下面这种:

...
<DataProvider>
  {data => (
    <Cat target={data.target} />
  )}
</DataProvider>

高阶组件这个概念就更好理解了,说白了就是一个函数接受一个组件作为参数,经过一系列加工后,最后返回一个新的组件。看下面的代码示例,withUser函数就是一个高阶组件,它返回了一个新的组件,这个组件具有了它提供的获取用户信息的功能。

const withUser = WrappedComponent => {
  const user = sessionStorage.getItem("user");
  return props => <WrappedComponent user={user} {...props} />;
};

const UserPage = props => (
  <div class="user-container">
    <p>My name is {props.user}!</p>
  </div>
);

export default withUser(UserPage);

以上这两种模式看上去都挺不错的,很多库也运用了这种模式,比如我们常用的React Router。但我们仔细看这两种模式,会发现它们会增加我们代码的层级关系。最直观的体现,打开devtool看看你的组件层级嵌套是不是很夸张吧。这时候再回过头看我们上一节给出的hooks例子,是不是简洁多了,没有多余的层级嵌套。把各种想要的功能写成一个一个可复用的自定义hook,当你的组件想用什么功能时,直接在组件里调用这个hook即可。

图片描述

生命周期钩子函数里的逻辑太乱了吧!

我们通常希望一个函数只做一件事情,但我们的生命周期钩子函数里通常同时做了很多事情。比如我们需要在componentDidMount中发起ajax请求获取数据,绑定一些事件监听等等。同时,有时候我们还需要在componentDidUpdate做一遍同样的事情。当项目变复杂后,这一块的代码也变得不那么直观。

classes真的太让人困惑了!

我们用class来创建react组件时,还有一件很麻烦的事情,就是this的指向问题。为了保证this的指向正确,我们要经常写这样的代码:this.handleClick = this.handleClick.bind(this),或者是这样的代码:<button onClick={() => this.handleClick(e)}>。一旦我们不小心忘了绑定this,各种bug就随之而来,很麻烦。

还有一件让我很苦恼的事情。我在之前的react系列文章当中曾经说过,尽可能把你的组件写成无状态组件的形式,因为它们更方便复用,可独立测试。然而很多时候,我们用function写了一个简洁完美的无状态组件,后来因为需求变动这个组件必须得有自己的state,我们又得很麻烦的把function改成class。

在这样的背景下,Hooks便横空出世了!

什么是State Hooks?

回到一开始我们用的例子,我们分解来看到底state hooks做了什么:

import { useState } from 'react';

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

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

声明一个状态变量

import { useState } from 'react';

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

useState是react自带的一个hook函数,它的作用就是用来声明状态变量。useState这个函数接收的参数是我们的状态初始值(initial state),它返回了一个数组,这个数组的第[0]项是当前当前的状态值,第[1]项是可以改变状态值的方法函数。

所以我们做的事情其实就是,声明了一个状态变量count,把它的初始值设为0,同时提供了一个可以更改count的函数setCount。

上面这种表达形式,是借用了es6的数组解构(array destructuring),它可以让我们的代码看起来更简洁。不清楚这种用法的可以先去看下我的这篇文章30分钟掌握ES6/ES2015核心内容(上)

如果不用数组解构的话,可以写成下面这样。实际上数组解构是一件开销很大的事情,用下面这种写法,或者改用对象解构,性能会有很大的提升。具体可以去这篇文章的分析Array destructuring for multi-value returns (in light of React hooks),这里不详细展开,我们就按照官方推荐使用数组解构就好。

let _useState = useState(0);
let count = _useState[0];
let setCount = _useState[1];

读取状态值

<p>You clicked {count} times</p>

是不是超简单?因为我们的状态count就是一个单纯的变量而已,我们再也不需要写成{this.state.count}这样了。

更新状态

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

当用户点击按钮时,我们调用setCount函数,这个函数接收的参数是修改过的新状态值。接下来的事情就交给react了,react将会重新渲染我们的Example组件,并且使用的是更新后的新的状态,即count=1。这里我们要停下来思考一下,Example本质上也是一个普通的函数,为什么它可以记住之前的状态?

一个至关重要的问题

这里我们就发现了问题,通常来说我们在一个函数中声明的变量,当函数运行完成后,这个变量也就销毁了(这里我们先不考虑闭包等情况),比如考虑下面的例子:

function add(n) {
    const result = 0;
    return result + 1;
}

add(1); //1
add(1); //1

不管我们反复调用add函数多少次,结果都是1。因为每一次我们调用add时,result变量都是从初始值0开始的。那为什么上面的Example函数每次执行的时候,都是拿的上一次执行完的状态值作为初始值?答案是:是react帮我们记住的。至于react是用什么机制记住的,我们可以再思考一下。

假如一个组件有多个状态值怎么办?

首先,useState是可以多次调用的,所以我们完全可以这样写:

function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

其次,useState接收的初始值没有规定一定要是string/number/boolean这种简单数据类型,它完全可以接收对象或者数组作为参数。唯一需要注意的点是,之前我们的this.setState做的是合并状态后返回一个新状态,而useState是直接替换老状态后返回新状态。最后,react也给我们提供了一个useReducer的hook,如果你更喜欢redux式的状态管理方案的话。

从ExampleWithManyStates函数我们可以看到,useState无论调用多少次,相互之间是独立的。这一点至关重要。为什么这么说呢?

其实我们看hook的“形态”,有点类似之前被官方否定掉的Mixins这种方案,都是提供一种“插拔式的功能注入”的能力。而mixins之所以被否定,是因为Mixins机制是让多个Mixins共享一个对象的数据空间,这样就很难确保不同Mixins依赖的状态不发生冲突。

而现在我们的hook,一方面它是直接用在function当中,而不是class;另一方面每一个hook都是相互独立的,不同组件调用同一个hook也能保证各自状态的独立性。这就是两者的本质区别了。

react是怎么保证多个useState的相互独立的?

还是看上面给出的ExampleWithManyStates例子,我们调用了三次useState,每次我们传的参数只是一个值(如42,‘banana’),我们根本没有告诉react这些值对应的key是哪个,那react是怎么保证这三个useState找到它对应的state呢?

答案是,react是根据useState出现的顺序来定的。我们具体来看一下:

  //第一次渲染
  useState(42);  //将age初始化为42
  useState('banana');  //将fruit初始化为banana
  useState([{ text: 'Learn Hooks' }]); //...

  //第二次渲染
  useState(42);  //读取状态变量age的值(这时候传的参数42直接被忽略)
  useState('banana');  //读取状态变量fruit的值(这时候传的参数banana直接被忽略)
  useState([{ text: 'Learn Hooks' }]); //...

假如我们改一下代码:

let showFruit = true;
function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  
  if(showFruit) {
    const [fruit, setFruit] = useState('banana');
    showFruit = false;
  }
 
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

这样一来,

  //第一次渲染
  useState(42);  //将age初始化为42
  useState('banana');  //将fruit初始化为banana
  useState([{ text: 'Learn Hooks' }]); //...

  //第二次渲染
  useState(42);  //读取状态变量age的值(这时候传的参数42直接被忽略)
  // useState('banana');  
  useState([{ text: 'Learn Hooks' }]); //读取到的却是状态变量fruit的值,导致报错

鉴于此,react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致。

什么是Effect Hooks?

我们在上一节的例子中增加一个新功能:

import { useState, useEffect } from 'react';

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

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

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

我们对比着看一下,如果没有hooks,我们会怎么写?

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

我们写的有状态组件,通常会产生很多的副作用(side effect),比如发起ajax请求获取数据,添加一些监听的注册和取消注册,手动修改dom等等。我们之前都把这些副作用的函数写在生命周期函数钩子里,比如componentDidMount,componentDidUpdate和componentWillUnmount。而现在的useEffect就相当与这些声明周期函数钩子的集合体。它以一抵三。

同时,由于前文所说hooks可以反复多次使用,相互独立。所以我们合理的做法是,给每一个副作用一个单独的useEffect钩子。这样一来,这些副作用不再一股脑堆在生命周期钩子里,代码变得更加清晰。

useEffect做了什么?

我们再梳理一遍下面代码的逻辑:

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

首先,我们声明了一个状态变量count,将它的初始值设为0。然后我们告诉react,我们的这个组件有一个副作用。我们给useEffecthook传了一个匿名函数,这个匿名函数就是我们的副作用。在这个例子里,我们的副作用是调用browser API来修改文档标题。当react要渲染我们的组件时,它会先记住我们用到的副作用。等react更新了DOM之后,它再依次执行我们定义的副作用函数。

这里要注意几点:
第一,react首次渲染和之后的每次渲染都会调用一遍传给useEffect的函数。而之前我们要用两个声明周期函数来分别表示首次渲染(componentDidMount),和之后的更新导致的重新渲染(componentDidUpdate)。

第二,useEffect中定义的副作用函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而之前的componentDidMount或componentDidUpdate中的代码则是同步执行的。这种安排对大多数副作用说都是合理的,但有的情况除外,比如我们有时候需要先根据DOM计算出某个元素的尺寸再重新渲染,这时候我们希望这次重新渲染是同步发生的,也就是说它会在浏览器真的去绘制这个页面前发生。

useEffect怎么解绑一些副作用

这种场景很常见,当我们在componentDidMount里添加了一个注册,我们得马上在componentWillUnmount中,也就是组件被注销之前清除掉我们添加的注册,否则内存泄漏的问题就出现了。

怎么清除呢?让我们传给useEffect的副作用函数返回一个新的函数即可。这个新的函数将会在组件下一次重新渲染之后执行。这种模式在一些pubsub模式的实现中很常见。看下面的例子:

import { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // 一定注意下这个顺序:告诉react在下次重新渲染组件之后,同时是下次调用ChatAPI.subscribeToFriendStatus之前执行cleanup
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

这里有一个点需要重视!这种解绑的模式跟componentWillUnmount不一样。componentWillUnmount只会在组件被销毁前执行一次而已,而useEffect里的函数,每次组件渲染后都会执行一遍,包括副作用函数返回的这个清理函数也会重新执行一遍。所以我们一起来看一下下面这个问题。

为什么要让副作用函数每次组件更新都执行一遍?

我们先看以前的模式:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

很清除,我们在componentDidMount注册,再在componentWillUnmount清除注册。但假如这时候props.friend.id变了怎么办?我们不得不再添加一个componentDidUpdate来处理这种情况:

...
  componentDidUpdate(prevProps) {
    // 先把上一个friend.id解绑
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // 再重新注册新但friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
...

看到了吗?很繁琐,而我们但useEffect则没这个问题,因为它在每次组件更新后都会重新执行一遍。所以代码的执行顺序是这样的:

1.页面首次渲染
2.替friend.id=1的朋友注册

3.突然friend.id变成了2
4.页面重新渲染
5.清除friend.id=1的绑定
6.替friend.id=2的朋友注册
...

怎么跳过一些不必要的副作用函数

按照上一节的思路,每次重新渲染都要执行一遍这些副作用函数,显然是不经济的。怎么跳过一些不必要的计算呢?我们只需要给useEffect传第二个参数即可。用第二个参数来告诉react只有当这个参数的值发生改变时,才执行我们传的副作用函数(第一个参数)。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 只有当count的值发生变化时,才会重新执行`document.title`这一句

当我们第二个参数传一个空数组[]时,其实就相当于只在首次渲染的时候执行。也就是componentDidMount加componentWillUnmount的模式。不过这种用法可能带来bug,少用。

还有哪些自带的Effect Hooks?

除了上文重点介绍的useState和useEffect,react还给我们提供来很多有用的hooks:

useContext
useReducer
useCallback
useMemo
useRef
useImperativeMethods
useMutationEffect
useLayoutEffect

我不再一一介绍,大家自行去查阅官方文档。

怎么写自定义的Effect Hooks?

为什么要自己去写一个Effect Hooks? 这样我们才能把可以复用的逻辑抽离出来,变成一个个可以随意插拔的“插销”,哪个组件要用来,我就插进哪个组件里,so easy!看一个完整的例子,你就明白了。

比如我们可以把上面写的FriendStatus组件中判断朋友是否在线的功能抽出来,新建一个useFriendStatus的hook专门用来判断某个id是否在线。

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

这时候FriendStatus组件就可以简写为:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

简直Perfect!假如这个时候我们又有一个朋友列表也需要显示是否在线的信息:

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

简直Fabulous!

结尾

不知道你阅读完整篇文章的感受如何,或者对hooks有任何角度的看法和思考都欢迎在评论区一起讨论。另外如果你有换工作的打算,我们部门真的很缺人,欢迎私信勾搭~(阿里巴巴,base在深圳的部门)

查看原文

赞 192 收藏 111 评论 10

RainyCG 赞了文章 · 2019-06-10

webpack原理

webpack原理

查看所有文档页面:前端开发文档,获取更多信息。
原文链接:webpack原理,原文广告模态框遮挡,阅读体验不好,所以整理成本文,方便查找。

工作原理概括

基本概念

在了解 Webpack 原理前,需要掌握以下几个核心概念,以方便后面的理解:

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。

流程概括

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

流程细节

Webpack 的构建流程可以分为以下三大阶段:

  1. 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
  2. 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
  3. 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。

如果只执行一次构建,以上阶段将会按照顺序各执行一次。但在开启监听模式下,流程将变为如下:

在每个大阶段中又会发生很多事件,Webpack 会把这些事件广播出来供给 Plugin 使用,下面来一一介绍。

初始化阶段

事件名解释
初始化参数从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。 这个过程中还会执行配置文件中的插件实例化语句 new Plugin()
实例化 Compiler用上一步得到的参数初始化 Compiler 实例,Compiler 负责文件监听和启动编译。Compiler 实例中包含了完整的 Webpack 配置,全局只有一个 Compiler 实例。
加载插件依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点。同时给插件传入 compiler 实例的引用,以方便插件通过 compiler 调用 Webpack 提供的 API。
environment开始应用 Node.js 风格的文件系统到 compiler 对象,以方便后续的文件寻找和读取。
entry-option读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备。
after-plugins调用完所有内置的和配置的插件的 apply 方法。
after-resolvers根据配置初始化完 resolverresolver 负责在文件系统中寻找指定路径的文件。
空格空格
空格空格
空格空格

编译阶段

事件名解释
run启动一次新的编译。
watch-runrun 类似,区别在于它是在监听模式下启动的编译,在这个事件中可以获取到是哪些文件发生了变化导致重新启动一次新的编译。
compile该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上 compiler 对象。
compilationWebpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。
make一个新的 Compilation 创建完毕,即将从 Entry 开始读取文件,根据文件类型和配置的 Loader 对文件进行编译,编译完后再找出该文件依赖的文件,递归的编译和解析。
after-compile一次 Compilation 执行完成。
invalid当遇到文件不存在、文件编译错误等异常时会触发该事件,该事件不会导致 Webpack 退出。
空格空格
空格空格

在编译阶段中,最重要的要数 compilation 事件了,因为在 compilation 阶段调用了 Loader 完成了每个模块的转换操作,在 compilation 阶段又包括很多小的事件,它们分别是:

事件名解释
build-module使用对应的 Loader 去转换一个模块。
normal-module-loader在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析。
program从配置的入口模块开始,分析其 AST,当遇到 require 等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。
seal所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk。

输出阶段

事件名解释
should-emit所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。
emit确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。
after-emit文件输出完毕。
done成功完成一次完成的编译和输出流程。
failed如果在编译和输出流程中遇到异常导致 Webpack 退出时,就会直接跳转到本步骤,插件可以在本事件中获取到具体的错误原因。

在输出阶段已经得到了各个模块经过转换后的结果和其依赖关系,并且把相关模块组合在一起形成一个个 Chunk。 在输出阶段会根据 Chunk 的类型,使用对应的模版生成最终要要输出的文件内容。

输出文件分析

虽然在前面的章节中你学会了如何使用 Webpack ,也大致知道其工作原理,可是你想过 Webpack 输出的 bundle.js 是什么样子的吗? 为什么原来一个个的模块文件被合并成了一个单独的文件?为什么 bundle.js 能直接运行在浏览器中? 本节将解释清楚以上问题。

先来看看由 安装与使用 中最简单的项目构建出的 bundle.js 文件内容,代码如下:

<p data-height="565" data-theme-id="0" data-slug-hash="NMQzxz" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="bundle.js" class="codepen">See the Pen bundle.js by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

以上看上去复杂的代码其实是一个立即执行函数,可以简写为如下:

(function(modules) {

  // 模拟 require 语句
  function __webpack_require__() {
  }

  // 执行存放所有模块数组中的第0个模块
  __webpack_require__(0);

})([/*存放所有模块的数组*/])

bundle.js 能直接运行在浏览器中的原因在于输出的文件中通过 __webpack_require__ 函数定义了一个可以在浏览器中执行的加载函数来模拟 Node.js 中的 require 语句。

原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。

如果仔细分析 __webpack_require__ 函数的实现,你还有发现 Webpack 做了缓存优化: 执行加载过的模块不会再执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。

分割代码时的输出

例如把源码中的 main.js 修改为如下:

// 异步加载 show.js
import('./show').then((show) => {
  // 执行 show 函数
  show('Webpack');
});

重新构建后会输出两个文件,分别是执行入口文件 bundle.js 和 异步加载文件 0.bundle.js

其中 0.bundle.js 内容如下:

// 加载在本文件(0.bundle.js)中包含的模块
webpackJsonp(
  // 在其它文件中存放着的模块的 ID
  [0],
  // 本文件所包含的模块
  [
    // show.js 所对应的模块
    (function (module, exports) {
      function show(content) {
        window.document.getElementById('app').innerText = 'Hello,' + content;
      }

      module.exports = show;
    })
  ]
);

bundle.js 内容如下:

<p data-height="565" data-theme-id="0" data-slug-hash="yjmRyG" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="bundle.js" class="codepen">See the Pen bundle.js by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

这里的 bundle.js 和上面所讲的 bundle.js 非常相似,区别在于:

  • 多了一个 __webpack_require__.e 用于加载被分割出去的,需要异步加载的 Chunk 对应的文件;
  • 多了一个 webpackJsonp 函数用于从异步加载的文件中安装模块。

在使用了 CommonsChunkPlugin 去提取公共代码时输出的文件和使用了异步加载时输出的文件是一样的,都会有 __webpack_require__.ewebpackJsonp。 原因在于提取公共代码和异步加载本质上都是代码分割。

编写 Loader

Loader 就像是一个翻译员,能把源文件经过转化后输出新的结果,并且一个文件还可以链式的经过多个翻译员翻译。

以处理 SCSS 文件为例:

  • SCSS 源代码会先交给 sass-loader 把 SCSS 转换成 CSS;
  • sass-loader 输出的 CSS 交给 css-loader 处理,找出 CSS 中依赖的资源、压缩 CSS 等;
  • css-loader 输出的 CSS 交给 style-loader 处理,转换成通过脚本加载的 JavaScript 代码;

可以看出以上的处理过程需要有顺序的链式执行,先 sass-loadercss-loaderstyle-loader。 以上处理的 Webpack 相关配置如下:

<p data-height="365" data-theme-id="0" data-slug-hash="YLmbeQ" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="编写 Loader" class="codepen">See the Pen 编写 Loader by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

Loader 的职责

由上面的例子可以看出:一个 Loader 的职责是单一的,只需要完成一种转换。 如果一个源文件需要经历多步转换才能正常使用,就通过多个 Loader 去转换。 在调用多个 Loader 去转换一个文件时,每个 Loader 会链式的顺序执行, 第一个 Loader 将会拿到需处理的原内容,上一个 Loader 处理后的结果会传给下一个接着处理,最后的 Loader 将处理后的最终结果返回给 Webpack。

所以,在你开发一个 Loader 时,请保持其职责的单一性,你只需关心输入和输出。

Loader 基础

由于 Webpack 是运行在 Node.js 之上的,一个 Loader 其实就是一个 Node.js 模块,这个模块需要导出一个函数。 这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。

一个最简单的 Loader 的源码如下:

module.exports = function(source) {
  // source 为 compiler 传递给 Loader 的一个文件的原内容
  // 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
  return source;
};

由于 Loader 运行在 Node.js 中,你可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用:

const sass = require('node-sass');
module.exports = function(source) {
  return sass(source);
};

Loader 进阶

以上只是个最简单的 Loader,Webpack 还提供一些 API 供 Loader 调用,下面来一一介绍。

获得 Loader 的 options

在最上面处理 SCSS 文件的 Webpack 配置中,给 css-loader 传了 options 参数,以控制 css-loader。 如何在自己编写的 Loader 中获取到用户传入的 options 呢?需要这样做:

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  // 获取到用户给当前 Loader 传入的 options
  const options = loaderUtils.getOptions(this);
  return source;
};

返回其它结果

上面的 Loader 都只是返回了原内容转换后的内容,但有些场景下还需要返回除了内容之外的东西。

例如以用 babel-loader 转换 ES6 代码为例,它还需要输出转换后的 ES5 代码对应的 Source Map,以方便调试源码。 为了把 Source Map 也一起随着 ES5 代码返回给 Webpack,可以这样写:

module.exports = function(source) {
  // 通过 this.callback 告诉 Webpack 返回的结果
  this.callback(null, source, sourceMaps);
  // 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
  // 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中 
  return;
};

其中的 this.callback 是 Webpack 给 Loader 注入的 API,以方便 Loader 和 Webpack 之间通信。 this.callback 的详细使用方法如下:

this.callback(
    // 当无法转换原内容时,给 Webpack 返回一个 Error
    err: Error | null,
    // 原内容转换后的内容
    content: string | Buffer,
    // 用于把转换后的内容得出原内容的 Source Map,方便调试
    sourceMap?: SourceMap,
    // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
    // 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
    abstractSyntaxTree?: AST
);
Source Map 的生成很耗时,通常在开发环境下才会生成 Source Map,其它环境下不用生成,以加速构建。 为此 Webpack 为 Loader 提供了 this.sourceMap API 去告诉 Loader 当前构建环境下用户是否需要 Source Map。 如果你编写的 Loader 会生成 Source Map,请考虑到这点。

同步与异步

Loader 有同步和异步之分,上面介绍的 Loader 都是同步的 Loader,因为它们的转换流程都是同步的,转换完成后再返回结果。 但在有些场景下转换的步骤只能是异步完成的,例如你需要通过网络请求才能得出结果,如果采用同步的方式网络请求就会阻塞整个构建,导致构建非常缓慢。

在转换步骤是异步时,你可以这样:

module.exports = function(source) {
    // 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
    var callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, ast) {
        // 通过 callback 返回异步执行后的结果
        callback(err, result, sourceMaps, ast);
    });
};

处理二进制数据

在默认的情况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串。 但有些场景下 Loader 不是处理文本文件,而是处理二进制文件,例如 file-loader,就需要 Webpack 给 Loader 传入二进制格式的数据。 为此,你需要这样编写 Loader:

module.exports = function(source) {
    // 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
    source instanceof Buffer === true;
    // Loader 返回的类型也可以是 Buffer 类型的
    // 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
    return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据 
module.exports.raw = true;

以上代码中最关键的代码是最后一行 module.exports.raw = true;,没有该行 Loader 只能拿到字符串。

缓存加速

在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。 为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。

如果你想让 Webpack 不缓存该 Loader 的处理结果,可以这样:

module.exports = function(source) {
  // 关闭该 Loader 的缓存功能
  this.cacheable(false);
  return source;
};

其它 Loader API

除了以上提到的在 Loader 中能调用的 Webpack API 外,还存在以下常用 API:

  • this.context:当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context 就等于 /src
  • this.resource:当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1
  • this.resourcePath:当前处理文件的路径,例如 /src/main.js
  • this.resourceQuery:当前处理文件的 querystring
  • this.target:等于 Webpack 配置中的 Target。
  • this.loadModule:但 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时, 就可以通过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果。
  • this.resolve:像 require 语句一样获得指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string))
  • this.addDependency:给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file: string)
  • this.addContextDependency:和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string)
  • this.clearDependencies:清除当前正在处理文件的所有依赖,使用方法为 clearDependencies()
  • this.emitFile:输出一个文件,使用方法为 emitFile(name: string, content: Buffer|string, sourceMap: {...})

加载本地 Loader

在开发 Loader 的过程中,为了测试编写的 Loader 是否能正常工作,需要把它配置到 Webpack 中后,才可能会调用该 Loader。 在前面的章节中,使用的 Loader 都是通过 Npm 安装的,要使用 Loader 时会直接使用 Loader 的名称,代码如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css/,
        use: ['style-loader'],
      },
    ]
  },
};

如果还采取以上的方法去使用本地开发的 Loader 将会很麻烦,因为你需要确保编写的 Loader 的源码是在 node_modules 目录下。 为此你需要先把编写的 Loader 发布到 Npm 仓库后再安装到本地项目使用。

解决以上问题的便捷方法有两种,分别如下:

Npm link

Npm link 专门用于开发和调试本地 Npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 Npm 模块。 由于是通过软链接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。

完成 Npm link 的步骤如下:

  • 确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
  • 在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
  • 在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 Npm 模块链接到项目的 node_moduels 下,其中的 loader-name 是指在第1步中的 package.json 文件中配置的模块名称。

链接好 Loader 到项目后你就可以像使用一个真正的 Npm 模块一样使用本地的 Loader 了。

ResolveLoader

ResolveLoader 用于配置 Webpack 如何寻找 Loader。 默认情况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 需要修改 resolveLoader.modules

假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:


module.exports = {
  resolveLoader:{
    // 去哪些目录下寻找 Loader,有先后顺序之分
    modules: ['node_modules','./loaders/'],
  }
}

加上以上配置后, Webpack 会先去 node_modules 项目下寻找 Loader,如果找不到,会再去 ./loaders/ 目录下寻找。

实战

上面讲了许多理论,接下来从实际出发,来编写一个解决实际问题的 Loader。

该 Loader 名叫 comment-require-loader,作用是把 JavaScript 代码中的注释语法:

// @require '../style/index.css'

转换成:

require('../style/index.css');

该 Loader 的使用场景是去正确加载针对 Fis3 编写的 JavaScript,这些 JavaScript 中存在通过注释的方式加载依赖的 CSS 文件。

该 Loader 的使用方法如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['comment-require-loader'],
        // 针对采用了 fis3 CSS 导入语法的 JavaScript 文件通过 comment-require-loader 去转换 
        include: [path.resolve(__dirname, 'node_modules/imui')]
      }
    ]
  }
};

该 Loader 的实现非常简单,完整代码如下:

function replace(source) {
    // 使用正则把 // @require '../style/index.css' 转换成 require('../style/index.css');  
    return source.replace(/(\/\/ *@require) +(('|").+('|")).*/, 'require($2);');
}

module.exports = function (content) {
    return replace(content);
};

编写 Plugin

Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

一个最基础的 Plugin 的代码是这样的:

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }

  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    compiler.plugin('compilation',function(compilation) {
    })
  }
}

// 导出 Plugin
module.exports = BasicPlugin;

在使用这个 Plugin 时,相关配置代码如下:

const BasicPlugin = require('./BasicPlugin.js');
module.export = {
  plugins:[
    new BasicPlugin(options),
  ]
}

Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例。 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。

通过以上最简单的 Plugin 相信你大概明白了 Plugin 的工作原理,但实际开发中还有很多细节需要注意,下面来详细介绍。

CompilerCompilation

在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。 Compiler 和 Compilation 的含义如下:

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 optionsloadersplugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

事件流

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。

Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

Webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。Compiler 和 Compilation 都继承自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听事件,方法如下:

/**
* 广播出事件
* event-name 为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name',params);

/**
* 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name',function(params) {

});

同理,compilation.applycompilation.plugin 使用方法和上面一致。

在开发插件时,你可能会不知道该如何下手,因为你不知道该监听哪个事件才能完成任务。

在开发插件时,还需要注意以下两点:

  • 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
  • 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
  • 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。例如:
 compiler.plugin('emit',function(compilation, callback) {
    // 支持处理逻辑

    // 处理完毕后执行 callback 以通知 Webpack 
    // 如果不执行 callback,运行流程将会一直卡在这不往下执行 
    callback();
  });

常用 API

插件可以用来修改输出文件、增加输出文件、甚至可以提升 Webpack 性能、等等,总之插件通过调用 Webpack 提供的 API 能完成很多事情。 由于 Webpack 提供的 API 非常多,有很多 API 很少用的上,又加上篇幅有限,下面来介绍一些常用的 API。

读取输出资源、代码块、模块及其依赖

有些插件可能需要读取 Webpack 的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理。

emit 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。 插件代码如下:

<p data-height="585" data-theme-id="0" data-slug-hash="RJwjPj" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="emit" class="codepen">See the Pen emit by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

监听文件变化

Webpack 会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation。

在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation,为此可以使用如下代码:

<p data-height="255" data-theme-id="0" data-slug-hash="jKOabJ" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="Compilation" class="codepen">See the Pen Compilation by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件。 由于 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会重新触发新的 Compilation。 为了监听 HTML 文件的变化,我们需要把 HTML 文件加入到依赖列表中,为此可以使用如下代码:

compiler.plugin('after-compile', (compilation, callback) => {
  // 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
    compilation.fileDependencies.push(filePath);
    callback();
});

修改输出资源

有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 emit 事件,因为发生 emit 事件时所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此 emit 事件是修改 Webpack 输出资源的最后时机。

所有需要输出的资源会存放在 compilation.assets 中,compilation.assets 是一个键值对,键为需要输出的文件名称,值为文件对应的内容。

设置 compilation.assets 的代码如下:

compiler.plugin('emit', (compilation, callback) => {
  // 设置名称为 fileName 的输出资源
  compilation.assets[fileName] = {
    // 返回文件内容
    source: () => {
      // fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
      return fileContent;
      },
    // 返回文件大小
      size: () => {
      return Buffer.byteLength(fileContent, 'utf8');
    }
  };
  callback();
});

读取 compilation.assets 的代码如下:


compiler.plugin('emit', (compilation, callback) => {
  // 读取名称为 fileName 的输出资源
  const asset = compilation.assets[fileName];
  // 获取输出资源的内容
  asset.source();
  // 获取输出资源的文件大小
  asset.size();
  callback();
});

判断 Webpack 使用了哪些插件

在开发一个插件时可能需要根据当前配置是否使用了其它某个插件而做下一步决定,因此需要读取 Webpack 当前的插件配置情况。 以判断当前是否使用了 ExtractTextPlugin 为例,可以使用如下代码:

// 判断当前配置使用使用了 ExtractTextPlugin,
// compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数
function hasExtractTextPlugin(compiler) {
  // 当前配置所有使用的插件列表
  const plugins = compiler.options.plugins;
  // 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
  return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null;
}

实战

下面我们举一个实际的例子,带你一步步去实现一个插件。

该插件的名称取名叫 EndWebpackPlugin,作用是在 Webpack 即将退出时再附加一些额外的操作,例如在 Webpack 成功编译和输出了文件后执行发布操作把输出的文件上传到服务器。 同时该插件还能区分 Webpack 构建是否执行成功。使用该插件时方法如下:

module.exports = {
  plugins:[
    // 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
    new EndWebpackPlugin(() => {
      // Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
    }, (err) => {
      // Webpack 构建失败,err 是导致错误的原因
      console.error(err);        
    })
  ]
}

要实现该插件,需要借助两个事件:

  • done:在成功构建并且输出了文件后,Webpack 即将退出时发生;
  • failed:在构建出现异常导致构建失败,Webpack 即将退出时发生;

实现该插件非常简单,完整代码如下:

class EndWebpackPlugin {

  constructor(doneCallback, failCallback) {
    // 存下在构造函数中传入的回调函数
    this.doneCallback = doneCallback;
    this.failCallback = failCallback;
  }

  apply(compiler) {
    compiler.plugin('done', (stats) => {
        // 在 done 事件中回调 doneCallback
        this.doneCallback(stats);
    });
    compiler.plugin('failed', (err) => {
        // 在 failed 事件中回调 failCallback
        this.failCallback(err);
    });
  }
}
// 导出插件 
module.exports = EndWebpackPlugin;

从开发这个插件可以看出,找到合适的事件点去完成功能在开发插件时显得尤为重要。 在 工作原理概括 中详细介绍过 Webpack 在运行过程中广播出常用事件,你可以从中找到你需要的事件。

调试 Webpack

在编写 Webpack 的 Plugin 和 Loader 时,可能执行结果会和你预期的不一样,就和你平时写代码遇到了奇怪的 Bug 一样。 对于无法一眼看出问题的 Bug,通常需要调试程序源码才能找出问题所在。

虽然可以通过 console.log 的方式完成调试,但这种方法非常不方便也不优雅,本节将教你如何断点调试 工作原理概括 中的插件代码。 由于 Webpack 运行在 Node.js 之上,调试 Webpack 就相对于调试 Node.js 程序。

在 Webstorm 中调试

Webstorm 集成了 Node.js 的调试工具,因此使用 Webstorm 调试 Webpack 非常简单。

1. 设置断点

在你认为可能出现问题的地方设下断点,点击编辑区代码左侧出现红点表示设置了断点。

2. 配置执行入口

告诉 Webstorm 如何启动 Webpack,由于 Webpack 实际上就是一个 Node.js 应用,因此需要新建一个 Node.js 类型的执行入口。

以上配置中有三点需要注意:

  • Name 设置成了 debug webpack,就像设置了一个别名,方便记忆和区分;
  • Working directory 设置为需要调试的插件所在的项目的根目录;
  • JavaScript file 即 Node.js 的执行入口文件,设置为 Webpack 的执行入口文件 node_modules/webpack/bin/webpack.js

3. 启动调试

经过以上两步,准备工作已经完成,下面启动调试,启动时选中前面设置的 debug webpack

4. 执行到断点

启动后程序就会停在断点所在的位置,在这里你可以方便的查看变量当前的状态,找出问题。

原理总结

Webpack 是一个庞大的 Node.js 应用,如果你阅读过它的源码,你会发现实现一个完整的 Webpack 需要编写非常多的代码。 但你无需了解所有的细节,只需了解其整体架构和部分细节即可。

对 Webpack 的使用者来说,它是一个简单强大的工具; 对 Webpack 的开发者来说,它是一个扩展性的高系统。

Webpack 之所以能成功,在于它把复杂的实现隐藏了起来,给用户暴露出的只是一个简单的工具,让用户能快速达成目的。 同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区补足了大量缺失的功能,让 Webpack 几乎能胜任任何场景。

通过本章的学习,希望你不仅能学会如何编写 Webpack 扩展,也能从中领悟到如何设计好的系统架构。

查看原文

赞 279 收藏 331 评论 21

RainyCG 发布了文章 · 2019-06-04

QQ音乐API koa2实现 - 全接口实现

QQMusicAPI

QQ音乐API koa2 版本, 通过Web网页版请求QQ音乐接口数据, 有问题请提 issue, 或者你有其他想法欢迎PR.

Github

知乎

掘金

环境要求

因为本项目采用的是koa2, 所以请确保你的node版本是7.6.0+
node -v

安装

git@github.com:Rain120/qq-music-api.git
npm install

项目启动

// npm i -g nodemon
npm run start

// or don't install nodemon
node app.js

项目监听端口是3200

使用文档

使用apis详见文档

关于本人

Rain120: 前端菜鸟, 入职前端1年, 公司的技术栈是React, 因为公司官网由我重构过, 我使用的Vue.js重构的。目前正在脱坑, 求大佬内推呀

API结构图

qq-music

API接口

koa接口说明(参数, 地址, 效果图)

获取QQ音乐产品的下载地址

接口说明: 调用此接口, 可获取QQ音乐标准产品下载链接

接口地址: /downloadQQMusic

调用例子: /downloadQQMusic

示例截图:

获取QQ音乐产品的下载地址

获取歌单分类

接口说明: 调用此接口, 可获取歌单分类, 包含category信息

接口地址: /getSongListCategories

调用例子: /getSongListCategories

<details>
<summary>SortID</summary>

sortId: 1, sortName: 默认
sortId: 2, sortName: 最新
sortId: 3, sortName: 最热
sortId: 4, sortName: 评分
sortId: 5, sortName: none

</details>

歌单分类(categoryId & categoryName)

<details>
<summary>1. 热门</summary>

1.1
  "categoryId": 10000000,
  "categoryName": 全部,

</details>

<details>
<summary>2. 语种</summary>

2.1
  "categoryId": 167,
  "categoryName": "英语",
2.2
  "categoryId": 168,
  "categoryName": "韩语",
2.3
  "categoryId": 166,
  "categoryName": "粤语",
2.4
  "categoryId": 169,
  "categoryName": "日语",
2.5
  "categoryId": 170,
  "categoryName": "小语种",
2.6
  "categoryId": 203,
  "categoryName": "闽南语",
2.7
  "categoryId": 204,
  "categoryName": "法语",
2.8
  "categoryId": 205,
  "categoryName": "拉丁语",

</details>

<details>
<summary>3. 流派</summary>

3.1
  "categoryId": 6,
  "categoryName": "流行",
3.2
  "categoryId": 15,
  "categoryName": "轻音乐",
3.3
  "categoryId": 11,
  "categoryName": "摇滚",
3.4
  "categoryId": 28,
  "categoryName": "民谣",
3.5
  "categoryId": 8,
  "categoryName": "R&B",
3.6
  "categoryId": 153,
  "categoryName": "嘻哈",
3.7
  "categoryId": 24,
  "categoryName": "电子",
3.8
  "categoryId": 27,
  "categoryName": "古典",
3.9
  "categoryId": 18,
  "categoryName": "乡村",
3.10
  "categoryId": 22,
  "categoryName": "蓝调",
3.11
  "categoryId": 21,
  "categoryName": "爵士",
3.12
  "categoryId": 164,
  "categoryName": "新世纪",
3.13
  "categoryId": 25,
  "categoryName": "拉丁",
3.14
  "categoryId": 218,
  "categoryName": "后摇",
3.15
  "categoryId": 219,
  "categoryName": "中国传统",
3.16
  "categoryId": 220,
  "categoryName": "世界音乐",

</details>

<details>
<summary>4. 主题</summary>

4.1
  "categoryId": 39,
  "categoryName": "ACG",
4.2
  "categoryId": 136,
  "categoryName": "经典",
4.3
  "categoryId": 146,
  "categoryName": "网络歌曲",
4.4
  "categoryId": 133,
  "categoryName": "影视",
4.5
  "categoryId": 141,
  "categoryName": "KTV热歌",
4.6
  "categoryId": 131,
  "categoryName": "儿歌",
4.7
  "categoryId": 145,
  "categoryName": "中国风",
4.8
  "categoryId": 194,
  "categoryName": "古风",
4.9
  "categoryId": 148,
  "categoryName": "情歌",
4.10
  "categoryId": 196,
  "categoryName": "城市",
4.11
  "categoryId": 197,
  "categoryName": "现场音乐",
4.12
  "categoryId": 199,
  "categoryName": "背景音乐",
4.13
  "categoryId": 200,
  "categoryName": "佛教音乐",
4.14
  "categoryId": 201,
  "categoryName": "UP主",
4.15
  "categoryId": 202,
  "categoryName": "乐器",
4.16
  "categoryId": 14,
  "categoryName": "DJ",

</details>

<details>
<summary>5. 心情</summary>

5.1
  "categoryId": 52,
  "categoryName": "伤感",
5.2
  "categoryId": 122,
  "categoryName": "安静",
5.3
  "categoryId": 117,
  "categoryName": "快乐",
5.4
  "categoryId": 116,
  "categoryName": "治愈",
5.5
  "categoryId": 125,
  "categoryName": "励志",
5.6
  "categoryId": 59,
  "categoryName": "甜蜜",
5.7
  "categoryId": 55,
  "categoryName": "寂寞",
5.8
  "categoryId": 126,
  "categoryName": "宣泄",
5.9
  "categoryId": 68,
  "categoryName": "思念",

</details>

<details>
<summary>6. 场景</summary>

6.1
  "categoryId": 78,
  "categoryName": "睡前",
6.2
  "categoryId": 102,
  "categoryName": "夜店",
6.3
  "categoryId": 101,
  "categoryName": "学习",
6.4
  "categoryId": 99,
  "categoryName": "运动",
6.5
  "categoryId": 99,
  "categoryName": "运动",
6.6
  "categoryId": 76,
  "categoryName": "约会",
6.7
  "categoryId": 94,
  "categoryName": "工作",
6.8
  "categoryId": 81,
  "categoryName": "旅行",
6.9
  "categoryId": 103,
  "categoryName": "派对",
6.10
  "categoryId": 222,
  "categoryName": "婚礼",
6.11
  "categoryId": 223,
  "categoryName": "咖啡馆",
6.12
  "categoryId": 224,
  "categoryName": "跳舞",
6.13
  "categoryId": 16,
  "categoryName": "校园",

</summary>
</details>

示例截图:

获取歌单分类

获取歌单列表

接口说明: 调用此接口, 可获取歌单列表

参数列表:

  • 必选参数

categoryId: 类别id

  • 可选参数

page: 当前页数, 默认为1

limit: 取出歌单数量, 默认为 20

sortId: 最新, 最热,评分, 默认为5

接口地址: /getSongLists

调用例子: /getSongLists?categoryId=10000000

示例截图:

获取歌单列表

获取歌单列表

获取歌单列表-带参数

获取歌单列表-带参数

获取歌单详情

接口说明: 调用此接口, 可获取歌单详情

参数列表:

  • 必选参数

disstid: 歌单id

接口地址: /getSongListDetail

调用例子: /getSongListDetail?disstid=7011264340

示例截图:

获取歌单详情

获取MV标签

接口说明: 调用此接口, 可获取MV标签

接口地址: /getMvByTag

调用例子: /getMvByTag

示例截图:

获取MV标签

获取MV播放信息

接口说明: 调用此接口, 可获取MV播放信息

参数列表:

  • 必选参数

vid: video id

接口地址: /getMvPlay

调用例子: /getMvPlay?vid=u00222le4ox

示例截图:

获取MV播放信息

获取歌手MV

接口说明: 调用此接口, 可获取歌手MV

参数列表:

  • 必选参数

singermid: 歌手id

  • 可选参数

order: 当前MV类型, 默认为time

  • listen: 歌手专辑音乐MV
  • time: 粉丝上传MV视频

limit: 取出歌单数量, 默认为5

接口地址: /getSingerMV

调用例子: /getSingerMV?singermid=0025NhlN2yWrP4&order=all&limit=5

示例截图:

获取歌手MV - default

获取歌手MV - belong

获取歌手MV - fans

获取相似歌手

接口说明: 调用此接口, 可获取相似歌手

参数列表:

  • 必选参数

singermid: 歌手id

接口地址: /getSimilarSinger

调用例子: /getSimilarSinger?singermid=0025NhlN2yWrP4

示例截图:

获取相似歌手

获取歌手信息

接口说明: 调用此接口, 可获取歌手信息

参数列表:

  • 必选参数

singermid: 歌手id

接口地址: /getSingerDesc

调用例子: /getSingerDesc?singermid=0025NhlN2yWrP4

示例截图:

获取歌手信息

获取歌手被关注数量信息

接口说明: 调用此接口, 可获取歌手被关注数量信息

参数列表:

  • 必选参数

singermid: 歌手id

接口地址: /getSingerStarNum

调用例子: /getSingerStarNum?singermid=0025NhlN2yWrP4

示例截图:

获取歌手被关注数量信息

获取电台列表

接口说明: 调用此接口, 可获取电台列表, 分类

接口地址: /getRadioLists

调用例子: /getRadioLists

示例截图:

获取电台列表

获取专辑

接口说明: 调用此接口, 可获取专辑信息(专辑列表、详情)

参数列表:

  • 必选参数

albummid: 专辑id

接口地址: /getAlbumInfo

调用例子: /getAlbumInfo?albummid=0016l2F430zMux

示例截图:

获取专辑

获取数字专辑

接口说明: 调用此接口, 可获取数字专辑, 轮播图banner, 专辑列表等信息, 详见API结构图

接口地址: /getDigitalAlbumLists

调用例子: /getDigitalAlbumLists

示例截图:

获取数字专辑

获取歌曲歌词

接口说明: 调用此接口, 可获取歌曲歌词

参数列表:

  • 必选参数

songmid: 专辑id

  • 可选参数

isFormat: 是否格式化歌词, 默认值为 false

接口地址: /getLyric

调用例子: /getLyric?songmid=003rJSwm3TechU

示例截图:

获取歌曲歌词 - 未格式化

获取歌曲歌词 - 格式化

获取MV

接口说明: 调用此接口, 可获取MV以及其Tag信息

参数列表:

  • 必选参数

area_id: 区域id, 默认值为全部(15)

<details>
<summary>Area</summary>

"area": [
  {
    "id": 15,
    "name": "全部"
  },
  {
    "id": 16,
    "name": "内地"
  },
  {
    "id": 17,
    "name": "港台"
  },
  {
    "id": 18,
    "name": "欧美"
  },
  {
    "id": 19,
    "name": "韩国"
  },
  {
    "id": 20,
    "name": "日本"
  }
]

</details>

version_id: 版本id, 默认值为全部(7)

<details>
<summary>Version</summary>

"version": [
  {
    "id": 7,
    "name": "全部"
  },
  {
    "id": 8,
    "name": "MV"
  },
  {
    "id": 9,
    "name": "现场"
  },
  {
    "id": 10,
    "name": "翻唱"
  },
  {
    "id": 11,
    "name": "舞蹈"
  },
  {
    "id": 12,
    "name": "影视"
  },
  {
    "id": 13,
    "name": "综艺"
  },
  {
    "id": 14,
    "name": "儿歌"
  }
]

</details>

  • 可选参数

page: 当前页数, 默认为1

limit: 取出歌单数量, 默认为 20

接口地址: /getMv

调用例子: /getMv

示例截图:

获取MV

获取新碟信息

接口说明: 调用此接口, 可获取新碟信息

参数列表:

  • 可选参数

page: 当前页数, 默认为1

limit: 取出歌单数量, 默认为 20

接口地址: /getNewDisks

调用例子: /getNewDisks

示例截图:

获取新碟信息

获取歌手专辑

接口说明: 调用此接口, 可获取歌手专辑

参数列表:

  • 必选参数

singermid: 歌手id

  • 可选参数

page: 当前页数, 默认为1

limit: 取出歌单数量, 默认为 20

接口地址: /getSingerAlbum

调用例子: /getSingerAlbum?singermid=0025NhlN2yWrP4

示例截图:

获取歌手专辑

获取歌曲VKey

接口说明: 调用此接口, 可获取歌曲VKey

参数列表:

  • 必选参数

songmid: 歌曲id

接口地址: /getMusicVKey

调用例子: /getMusicVKey?songmid=0025NhlN2yWrP4

示例截图:

获取歌曲VKey

获取搜索热词

接口说明: 调用此接口, 可获取搜索热词

接口地址: /getHotkey

调用例子: /getHotkey

示例截图:

获取搜索热词

获取关键字搜索提示

接口说明: 调用此接口, 可获取获取关键字搜索提示

参数列表:

  • 必选参数

key: 搜索关键字

接口地址: /getSmartbox

调用例子: /getSmartbox?key=周杰伦

示例截图:

获取获取关键字搜索提示

获取搜索结果

接口说明: 调用此接口, 可获取获取搜索结果

参数列表(部分参数待注释):

  • 必选参数

key: 搜索关键字

catZhida: 0表示歌曲, 2表示歌手, 3表示专辑, 默认值为1

  • 可选参数

page: 当前页数, 默认为1

limit: 取出歌单数量, 默认为 10

接口地址: /getSearchByKey

调用例子: /getSearchByKey?key=周杰伦

示例截图:

获取获取搜索结果

获取首页推荐

接口说明: 调用此接口, 可获取首页推荐

接口地址: /getRecommend

调用例子: /getRecommend

示例截图:

获取首页推荐

获取排行榜单列表

接口说明: 调用此接口, 可获取排行榜单列表

  • 可选参数

page: 当前页数, 默认为1

limit: 取出歌单数量, 默认为 10

接口地址: /getTopLists

调用例子: /getTopLists

示例截图:

获取排行榜单列表

获取排行榜单详情

接口说明: 调用此接口, 可获取排行榜单详情

  • 可选参数

topId: 榜单id

page: 当前页数, 默认为1

limit: 取出歌单数量, 默认为 10

接口地址: /getRanks

调用例子: /getRanks

示例截图:

获取排行榜单详情

获取评论信息(cmd代表的意思没太弄明白)

接口说明: 调用此接口, 可获取评论信息

  • 可选参数

id: 专辑或者歌单请求结果的id

  • 可选参数

rootcommentid: 榜单id

cid:

pagenum: 当前页数, 默认为0

pagesize: 取出评论数量, 默认为 25

cmd:

reqtype:

biztype:

接口地址: /getComments

调用例子: /getComments?id=8220&rootcommentid=album_8220_1003310416_1558068713

示例截图:

获取评论信息 - id获取

获取评论信息

获取评论信息 - 带params

获取票务信息

接口说明: 调用此接口, 可获取票务信息

接口地址: /getTicketInfo

调用例子: /getTicketInfo

示例截图:

获取票务信息

关于项目

灵感来自

Binaryify/NeteaseCloudMusicApi

Vue2.0开发企业级移动端音乐Web App

参考内容

Koa 2

Axios

阮一峰老师 - HTTP Referer 教程

项目不足

  1. 因为本人没写过unit test, 所以本项目尚未添加unit test, 等有时间再添加;
  2. 登录获取个人信息等接口都没做
查看原文

赞 36 收藏 27 评论 0

RainyCG 关注了问题 · 2019-05-16

npm run dev 报错

clipboard.png

clipboard.png

clipboard.png

Starting dev server...
ERROR Failed to compile with 1 errors16:56:05

This relative module was not found:

  • ./src/main.js in multi ./build/dev-client ./src/main.js

问题:每次运行my-mall总是这个报错,但是运行my-project就没有问题,求大佬解决
两个项目中的文件配置信息一模一样

关注 1 回答 1

RainyCG 评论了文章 · 2019-05-10

你必须知道的Git命令

这篇笔记是为了学习Git知识而收集总结的,主要是看受一篇帖子《你可能不知道的15条Git命令》的影响,才想记录这篇笔记的,如有雷同,纯属巧合。

Git 是一个分布式版本控制软件, 最初目的是为更好地管理Linux内核开发而设计。

来源:维基百科 - Git

Git是一个软件,它允许你通过提交对一个系统(或一组)文件的历史进行注释。这些提交便是在给定时间点对系统做出的差异“快照”。

官网下载速度慢,可使用这个链接下载 或者Github下载地址, 需要其他版本请提issue联系我。

1. Git 配置

--system #系统级别
--global #用户全局
--local #单独一个项目

git config --global user.name "xxxx" #用户名
git config --global user.email "xxxx@xxx.com" #邮箱

git config --list # 列举所有配置

连接远程仓库github

  1. 创建SSH Key

    ssh-keygen -t rsa -C <youremail@example.com>
  2. 登陆GitHub,打开Account settings -> SSH Keys -> Add SSH Key,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容
  3. 测试是否连接

    ssh git@github.com

几个概念:

工作区(Working Directory): 你在电脑里能看到的目录。

暂存区(stage / index): 保存了下次将提交的文件列表信息, 一般存放在 .git目录下 下的index文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)

版本库(Repository): 工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。

远程仓库(Remote)

阮一峰老师对codeGit/code工作区、暂存区、版本库、远程仓库的解释

阮一峰老师对Git工作区、暂存区、版本库、远程仓库的解释

codeRunoob/code对codeGit/code工作区、暂存区、版本库、远程仓库的解释

Runoob对Git工作区、暂存区、版本库、远程仓库的解释

忽略文件配置:添加.gitignore文件

文件 .gitignore 的格式规范如下:

  • 所有空行或者以 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配。
  • 匹配模式可以以(/)开头防止递归。
  • 匹配模式可以以(/)结尾指定目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。

2. 创建版本库

版本库又名仓库,英文名repository,你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。

git clone url # clone远程仓库
git init # 初始化本地版本库

3. Git 分支

master: 默认开发分支

HEAD: 当前开发分支

HEAD^([n]): HEAD的第n次父提交提交, ^相当于^1

HEAD~([n]): HEAD的第n个祖先提交

origin: 默认远程版本库

graph TD;
    br1_c1-->master*;
    br2_c2-->master*;
    br3_c3-->master*;
    br1_c1_c1-->br1_c1;
    br2_c2_c2-->br2_c2;
    br3_c3_c3-->br3_c3;

HEAD-diff

master: master分支

*: HEAD,当前活跃(开发)分支

br1_c1: br1分支的提交第一次提交

br1_c1_c1: br1_c1的第一次提交

如何区分`^`和`~`?
据上图示知,当前开发分支是`master`,即`HEAD`指向`master`, `c1`, `c2`, `c3`是`master`的三次父提交
`HEAD^ -> c1`,`HEAD^2 -> c2`,`HEAD^3 -> c3`, `HEAD~ -> c1`,`HEAD~2 -> c1_c1`
git branch # 查看分支

git branch -r #查看远程分支

# 此命令将显示包含特定提交的所有分支。
git branch --contains <commit>

git branch <name> # 创建分支

git checkout <name> # 切换分支

git checkout -b <name> # 创建 + 切换分支

# 重命名本地分支
git branch -m <old-name> <new-name>

# 重命名刚切换的新分支
git branch -m <new-name>

# 重命名远程分支: 一旦在本地重命名了分支,您需要先远程删除该分支,然后再次推送重命名的分支。
git push origin :<old-name>
git push origin <new-name>

git merge <name> # 合并某分支到当前分支

git branch -d <name> # 删除分支

git branch -D <name> # 强制删除分支

# 删除远程分支(先在本地删除该分支),原理是把一个空分支push到server上,相当于删除该分支。
git push origin :<name>

Note: checkout只会移动HEAD指针,reset会改变HEAD的引用值

4. 查看

git status # 查看状态

git diff <filename> # 查看修改内容

git diff <first_branch>..<second_branch> # 显示两次提交之间的差异

git diff --shortstat "@{n day ago}" # 显示n天的代码数量

git diff --cached(--staged) # 查看已经暂存起来的变化

git show <commit>:<filename> # 显示某次提交时,某个文件的内容

git show <commit> # 显示某次提交的元数据和内容变化

git show --name-only <commit> # 显示某次提交发生变化的文件

git reflog # 显示当前分支的最近几次提交

git blame <filename> # 显示指定文件修改信息

5. 修改

git add <filename>|<div> # 添加指定文件, 指定目录(包括子目录)到暂存区

git add . # 添加当前目录的所有文件到暂存区

# -p(或-patch)允许交互选择要提交的每个跟踪文件的各个部分。 这样每个提交只包含相关的更改。
git add -p

git mv <old-name> <new-name> # 文件改名

git rm --cached <file> # 停止追踪指定文件,但该文件会保留在工作区

git rm -f <filename> # 强制删除选项 -f

6. 提交

git commit -m 'message' # 提交版本库

git commit -a -m 'message' # 添加所有修改文件到暂存区,并提交版本库(不包括新增文件)

git commit --amend -m # 修改最后一次提交,如果代码没有任何新变化,则用来改写上一次commit的提交信息

Git Commit

7. 撤销、版本回滚

git reset --hard HEAD # 撤销工作目录中暂存的所有未提交文件的修改内容

git reset --keep [commit] # 重置当前HEAD为指定commit,但保持暂存区和工作区不变

git reset [file] # 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变

git commit --amend # 将暂存区中的文件提交

git checkout [file] # 恢复暂存区的指定文件到工作区

# --patch还可用于选择性地丢弃每个被跟踪文件的部分。
git checkout -p

# 此命令允许您快速切换到先前检出的分支。 一般说来 - 是前一个分支的别名。 它也可以与其他命令一起使用。
git checkout -

# 还原所有本地更改,如果您确定可以丢弃所有本地更改,则可以使用。
git checkout .

git checkout HEAD <filename> # 取消指定未提交文件的修改内容

git checkout --patch <filename> # 撤消对文件的修改

git revert <commit_id> # 撤销指定提交

8. 提交历史

常见参数选项:

-p: 显示每次提交的内容差异。

—stat: 显示每次更新的文件修改统计信息。

—shortstat: 只显示 —stat 中最后的行数修改添加移除统计。

--name-only 仅在提交信息后显示已修改的文件清单。

--name-status 显示新增、修改、删除的文件清单。

--abbrev-commit: 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。

--relative-date: 使用较短的相对时间显示(比如,2 weeks ago)。

--graph: 显示 ASCII 图形表示的分支合并历史。

—pretty=(oneline,short,medium(默认值),full,fuller,email,raw,format): 这个选项可以指定使用不同于默认格式的方式展示提交历史。 这个选项有一些内建的子选项供你使用。

- `oneline`: 将每个提交放在一行显示,查看的提交数很大时非常有用。
- [`format`](https://git-scm.com/book/zh/v2/Git-基础-查看提交历史#rpretty_format): 列出了常用的格式占位符写法及其代表的意义。

—oneline: --pretty=oneline --abbrev-commit 的简化用法。

--date= (relative|local|default|iso|rfc|short|raw):定制出现日期格式。

常见输出参数:

-n: 仅显示最近的 n 条提交

—since, —after: 仅显示指定时间之后的提交

--until, —before: 仅显示指定作者相关的提交。

—author: 仅显示指定提交者相关的提交。

—grep: 仅显示含指定关键字的提交

-S: 仅显示添加或移除了某个关键字的提交

默认不用任何参数的话,git log 会按提交时间列出所有的更新,最近的更新排在最上面。

git log # 查看所有提交历史

git log -p -n # 查看最近提交的n条历史

git log -p -n <filename> # 查看指定文件最近提交的n条历史

git log alias配置

git log --pretty=format:'%s %C(bold blue)(%an)%Creset' --abbrev-commit

git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit

# 设置git alias
git config --global alias.slg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
# 显示每个提交在过去两周内引入的差异日志。
git whatchanged —-since='2 weeks ago'

9. 标签

Git 可以给历史中的某一个提交打上标签,以示重要。

Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)

轻量标签: 很像一个不会改变的分支 - 它只是一个特定提交的引用。它本质上是将提交校验和存储到一个文件中 - 没有保存任何其他信息。

附注标签是存储在 Git 数据库中的一个完整对象。 它们是可以被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;并且可以使用 GNU Privacy Guard(GPG)签名与验证。 通常建议创建附注标签,这样你可以拥有以上所有信息;但是如果你只是想用一个临时的标签,或者因为某些原因不想要保存那些信息,轻量标签也是可用的。

-a: 创建附注标签

-m 选项指定了一条将会存储在标签中的信息。 如果没有为附注标签指定一条信息,Git 会运行编辑器要求你输入信息。

git tag # 列出已有的标签

git tag <tagname> # 创建标签,-a 创建附注标签

git tag -d <tagname> # 删除掉你本地仓库上的标签

git show <tagname> # 查看标签信息与对应的提交信息

git push origin <tagname> # 推送标签到远程仓库服务器上

git push origin --tags # 一次性推送所有不在远程仓库服务器上的标签

10. 变基

merge: 用来合并一个或者多个分支到你已经检出的分支中, 然后它将当前分支指针移动到合并结果上,现有分支不会被修改。

rebase: 通常称之为“衍合”,它通过修改提交历史来对比双方的commit,然后找出不同的去缓存,然后在去push,修改你的commit历史。

cherry-pick: 用于将在其他分支上的 commit 修改,移植到当前的分支(HEAD), -x 参数,表示保留原提交的作者信息进行提交。

git merge <branch> # 合并指定分支到当前分支

git rebase <branch> # 衍合指定分支到当前分支

# 用于将在其他分支上的 commit 修改,移植到当前的分支(HEAD), <start-commit-id>…<end-commit-id>左开右闭,<start-commit-id>^…<end-commit-id>全闭
git cherry-pick <start-commit-id>…<end-commit-id>

11. 储藏与清理

当你在项目的一部分上已经工作一段时间后,所有东西都进入了混乱的状态,而这时你想要切换到另一个分支做一点别的事情。 问题是,你不想仅仅因为过会儿回到这一点而为做了一半的工作创建一次提交。

储藏会处理工作目录的脏的状态 - 即,修改的跟踪文件与暂存改动 - 然后将未完成的修改保存到一个栈上,而你可以在任何时候重新应用这些改动。

git add . && git stash # 将新的储藏推送到栈上

git stash save 'message' # 储藏修改,并留下stash信息

# -p(或-patch)允许交互选择要提交的每个跟踪文件的各个部分。 这样每个提交只包含相关的更改。
git stash -p

# 默认情况下,当存储时,不包括未跟踪的文件。 为了更改该行为并包含这些文件,您需要使用-u参数。 还有-a(-all)可以完全存储未跟踪和忽略的文件,这可能是您通常不需要的东西。
git stash -u

git stash list # 查看栈中所有暂存

git stash apply <stash_id> # 恢复复对应编号暂存到工作区,如果不指定编号为栈顶的,注意:这些暂存还在栈中

git stash pop <stash_id> #将栈顶的暂存,恢复到工作区,并从栈中弹出,注意:这些暂存不在栈中

git stash drop <stash_id> # 移除的储藏在栈中的东西

git stash clear #清空暂存栈

git stash branch <branch_name> # 从储藏创建一个分支

12. 远程操作

远程仓库是指托管在因特网或其他网络中的你的项目的版本库。

git remote # 查看已配置的远程仓库服务器

git remote -v # 指定选项 -v,会显示需要读写远程仓库使用的 Git 保存的简写与其对应的 URL。

git remote show <remote-name> # 查看制定远程仓库的更多信息

git remote add <shortname> <url> # 添加一个新的远程 Git 仓库

git remote rm <name> # 移除远程仓库

git remote rename <oldname> <newname># 重命名远程仓库

git fetch <remote-name> # 从远程仓库中拉取数据

# 下载代码及快速合并, 当你想拉取origin服务器上的当前分支名的代码时,可简写git pull
git pull <remote-name> <branch-name>

# 第一次推送到远程仓库,--set-upstream可简写为-u
git push --set-upstream <remote-name> <branch-name>

# 推送到远程仓库, 当你想将当前开发分支名推送到 origin 服务器,可简写为git push
git push <remote> <branch-name>

git push [remote] --force # 强行推送当前分支到远程仓库,即使有冲突

git push [remote] --all # 推送所有分支到远程仓库

git push <remote> :<branch-name/tag-name> # 删除远程分支或标签

git push --tag # 上传所有标签

13. 冲突

# 一次打开所有冲突的文件,重新绑定可能会导致冲突,以下命令将打开需要您帮助解决这些冲突的所有文件。
git diff --name-only --diff-filter=U | uniq  | xargs $EDITOR

从本地初始化到上传到GitHub远程仓库的一般步骤

git init
git add .
git commit -m 'hint message'
git remote add origin 'your project repositories href on github'
git pull origin master
git push -u origin master

首次push远程仓库提交错误 -> 错误截图解决详见

error: failed to push some refs to 'https://github.com/xxx.git'
# --allow-unrelated-histories 合并了两个不相关的项目的历史记录。
git pull origin master --allow-unrelated-histories

git push -u origin master

git_commands

参考资料

Git Document中文文档

15-git-commands-you-may-not-know

阮一峰常用Git 命令清单

廖雪峰 Git教程

Runoob Git教程

Git入门

Github, 知乎,掘金,素质三连击~~

Github

知乎

掘金

查看原文

RainyCG 发布了文章 · 2019-05-08

你必须知道的Git命令

这篇笔记是为了学习Git知识而收集总结的,主要是看受一篇帖子《你可能不知道的15条Git命令》的影响,才想记录这篇笔记的,如有雷同,纯属巧合。

Git 是一个分布式版本控制软件, 最初目的是为更好地管理Linux内核开发而设计。

来源:维基百科 - Git

Git是一个软件,它允许你通过提交对一个系统(或一组)文件的历史进行注释。这些提交便是在给定时间点对系统做出的差异“快照”。

官网下载速度慢,可使用这个链接下载 或者Github下载地址, 需要其他版本请提issue联系我。

1. Git 配置

--system #系统级别
--global #用户全局
--local #单独一个项目

git config --global user.name "xxxx" #用户名
git config --global user.email "xxxx@xxx.com" #邮箱

git config --list # 列举所有配置

连接远程仓库github

  1. 创建SSH Key

    ssh-keygen -t rsa -C <youremail@example.com>
  2. 登陆GitHub,打开Account settings -> SSH Keys -> Add SSH Key,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容
  3. 测试是否连接

    ssh git@github.com

几个概念:

工作区(Working Directory): 你在电脑里能看到的目录。

暂存区(stage / index): 保存了下次将提交的文件列表信息, 一般存放在 .git目录下 下的index文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)

版本库(Repository): 工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。

远程仓库(Remote)

阮一峰老师对codeGit/code工作区、暂存区、版本库、远程仓库的解释

阮一峰老师对Git工作区、暂存区、版本库、远程仓库的解释

codeRunoob/code对codeGit/code工作区、暂存区、版本库、远程仓库的解释

Runoob对Git工作区、暂存区、版本库、远程仓库的解释

忽略文件配置:添加.gitignore文件

文件 .gitignore 的格式规范如下:

  • 所有空行或者以 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配。
  • 匹配模式可以以(/)开头防止递归。
  • 匹配模式可以以(/)结尾指定目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。

2. 创建版本库

版本库又名仓库,英文名repository,你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。

git clone url # clone远程仓库
git init # 初始化本地版本库

3. Git 分支

master: 默认开发分支

HEAD: 当前开发分支

HEAD^([n]): HEAD的第n次父提交提交, ^相当于^1

HEAD~([n]): HEAD的第n个祖先提交

origin: 默认远程版本库

graph TD;
    br1_c1-->master*;
    br2_c2-->master*;
    br3_c3-->master*;
    br1_c1_c1-->br1_c1;
    br2_c2_c2-->br2_c2;
    br3_c3_c3-->br3_c3;

HEAD-diff

master: master分支

*: HEAD,当前活跃(开发)分支

br1_c1: br1分支的提交第一次提交

br1_c1_c1: br1_c1的第一次提交

如何区分`^`和`~`?
据上图示知,当前开发分支是`master`,即`HEAD`指向`master`, `c1`, `c2`, `c3`是`master`的三次父提交
`HEAD^ -> c1`,`HEAD^2 -> c2`,`HEAD^3 -> c3`, `HEAD~ -> c1`,`HEAD~2 -> c1_c1`
git branch # 查看分支

git branch -r #查看远程分支

# 此命令将显示包含特定提交的所有分支。
git branch --contains <commit>

git branch <name> # 创建分支

git checkout <name> # 切换分支

git checkout -b <name> # 创建 + 切换分支

# 重命名本地分支
git branch -m <old-name> <new-name>

# 重命名刚切换的新分支
git branch -m <new-name>

# 重命名远程分支: 一旦在本地重命名了分支,您需要先远程删除该分支,然后再次推送重命名的分支。
git push origin :<old-name>
git push origin <new-name>

git merge <name> # 合并某分支到当前分支

git branch -d <name> # 删除分支

git branch -D <name> # 强制删除分支

# 删除远程分支(先在本地删除该分支),原理是把一个空分支push到server上,相当于删除该分支。
git push origin :<name>

Note: checkout只会移动HEAD指针,reset会改变HEAD的引用值

4. 查看

git status # 查看状态

git diff <filename> # 查看修改内容

git diff <first_branch>..<second_branch> # 显示两次提交之间的差异

git diff --shortstat "@{n day ago}" # 显示n天的代码数量

git diff --cached(--staged) # 查看已经暂存起来的变化

git show <commit>:<filename> # 显示某次提交时,某个文件的内容

git show <commit> # 显示某次提交的元数据和内容变化

git show --name-only <commit> # 显示某次提交发生变化的文件

git reflog # 显示当前分支的最近几次提交

git blame <filename> # 显示指定文件修改信息

5. 修改

git add <filename>|<div> # 添加指定文件, 指定目录(包括子目录)到暂存区

git add . # 添加当前目录的所有文件到暂存区

# -p(或-patch)允许交互选择要提交的每个跟踪文件的各个部分。 这样每个提交只包含相关的更改。
git add -p

git mv <old-name> <new-name> # 文件改名

git rm --cached <file> # 停止追踪指定文件,但该文件会保留在工作区

git rm -f <filename> # 强制删除选项 -f

6. 提交

git commit -m 'message' # 提交版本库

git commit -a -m 'message' # 添加所有修改文件到暂存区,并提交版本库(不包括新增文件)

git commit --amend -m # 修改最后一次提交,如果代码没有任何新变化,则用来改写上一次commit的提交信息

Git Commit

7. 撤销、版本回滚

git reset --hard HEAD # 撤销工作目录中暂存的所有未提交文件的修改内容

git reset --keep [commit] # 重置当前HEAD为指定commit,但保持暂存区和工作区不变

git reset [file] # 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变

git commit --amend # 将暂存区中的文件提交

git checkout [file] # 恢复暂存区的指定文件到工作区

# --patch还可用于选择性地丢弃每个被跟踪文件的部分。
git checkout -p

# 此命令允许您快速切换到先前检出的分支。 一般说来 - 是前一个分支的别名。 它也可以与其他命令一起使用。
git checkout -

# 还原所有本地更改,如果您确定可以丢弃所有本地更改,则可以使用。
git checkout .

git checkout HEAD <filename> # 取消指定未提交文件的修改内容

git checkout --patch <filename> # 撤消对文件的修改

git revert <commit_id> # 撤销指定提交

8. 提交历史

常见参数选项:

-p: 显示每次提交的内容差异。

—stat: 显示每次更新的文件修改统计信息。

—shortstat: 只显示 —stat 中最后的行数修改添加移除统计。

--name-only 仅在提交信息后显示已修改的文件清单。

--name-status 显示新增、修改、删除的文件清单。

--abbrev-commit: 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。

--relative-date: 使用较短的相对时间显示(比如,2 weeks ago)。

--graph: 显示 ASCII 图形表示的分支合并历史。

—pretty=(oneline,short,medium(默认值),full,fuller,email,raw,format): 这个选项可以指定使用不同于默认格式的方式展示提交历史。 这个选项有一些内建的子选项供你使用。

- `oneline`: 将每个提交放在一行显示,查看的提交数很大时非常有用。
- [`format`](https://git-scm.com/book/zh/v2/Git-基础-查看提交历史#rpretty_format): 列出了常用的格式占位符写法及其代表的意义。

—oneline: --pretty=oneline --abbrev-commit 的简化用法。

--date= (relative|local|default|iso|rfc|short|raw):定制出现日期格式。

常见输出参数:

-n: 仅显示最近的 n 条提交

—since, —after: 仅显示指定时间之后的提交

--until, —before: 仅显示指定作者相关的提交。

—author: 仅显示指定提交者相关的提交。

—grep: 仅显示含指定关键字的提交

-S: 仅显示添加或移除了某个关键字的提交

默认不用任何参数的话,git log 会按提交时间列出所有的更新,最近的更新排在最上面。

git log # 查看所有提交历史

git log -p -n # 查看最近提交的n条历史

git log -p -n <filename> # 查看指定文件最近提交的n条历史

git log alias配置

git log --pretty=format:'%s %C(bold blue)(%an)%Creset' --abbrev-commit

git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit

# 设置git alias
git config --global alias.slg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
# 显示每个提交在过去两周内引入的差异日志。
git whatchanged —-since='2 weeks ago'

9. 标签

Git 可以给历史中的某一个提交打上标签,以示重要。

Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)

轻量标签: 很像一个不会改变的分支 - 它只是一个特定提交的引用。它本质上是将提交校验和存储到一个文件中 - 没有保存任何其他信息。

附注标签是存储在 Git 数据库中的一个完整对象。 它们是可以被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;并且可以使用 GNU Privacy Guard(GPG)签名与验证。 通常建议创建附注标签,这样你可以拥有以上所有信息;但是如果你只是想用一个临时的标签,或者因为某些原因不想要保存那些信息,轻量标签也是可用的。

-a: 创建附注标签

-m 选项指定了一条将会存储在标签中的信息。 如果没有为附注标签指定一条信息,Git 会运行编辑器要求你输入信息。

git tag # 列出已有的标签

git tag <tagname> # 创建标签,-a 创建附注标签

git tag -d <tagname> # 删除掉你本地仓库上的标签

git show <tagname> # 查看标签信息与对应的提交信息

git push origin <tagname> # 推送标签到远程仓库服务器上

git push origin --tags # 一次性推送所有不在远程仓库服务器上的标签

10. 变基

merge: 用来合并一个或者多个分支到你已经检出的分支中, 然后它将当前分支指针移动到合并结果上,现有分支不会被修改。

rebase: 通常称之为“衍合”,它通过修改提交历史来对比双方的commit,然后找出不同的去缓存,然后在去push,修改你的commit历史。

cherry-pick: 用于将在其他分支上的 commit 修改,移植到当前的分支(HEAD), -x 参数,表示保留原提交的作者信息进行提交。

git merge <branch> # 合并指定分支到当前分支

git rebase <branch> # 衍合指定分支到当前分支

# 用于将在其他分支上的 commit 修改,移植到当前的分支(HEAD), <start-commit-id>…<end-commit-id>左开右闭,<start-commit-id>^…<end-commit-id>全闭
git cherry-pick <start-commit-id>…<end-commit-id>

11. 储藏与清理

当你在项目的一部分上已经工作一段时间后,所有东西都进入了混乱的状态,而这时你想要切换到另一个分支做一点别的事情。 问题是,你不想仅仅因为过会儿回到这一点而为做了一半的工作创建一次提交。

储藏会处理工作目录的脏的状态 - 即,修改的跟踪文件与暂存改动 - 然后将未完成的修改保存到一个栈上,而你可以在任何时候重新应用这些改动。

git add . && git stash # 将新的储藏推送到栈上

git stash save 'message' # 储藏修改,并留下stash信息

# -p(或-patch)允许交互选择要提交的每个跟踪文件的各个部分。 这样每个提交只包含相关的更改。
git stash -p

# 默认情况下,当存储时,不包括未跟踪的文件。 为了更改该行为并包含这些文件,您需要使用-u参数。 还有-a(-all)可以完全存储未跟踪和忽略的文件,这可能是您通常不需要的东西。
git stash -u

git stash list # 查看栈中所有暂存

git stash apply <stash_id> # 恢复复对应编号暂存到工作区,如果不指定编号为栈顶的,注意:这些暂存还在栈中

git stash pop <stash_id> #将栈顶的暂存,恢复到工作区,并从栈中弹出,注意:这些暂存不在栈中

git stash drop <stash_id> # 移除的储藏在栈中的东西

git stash clear #清空暂存栈

git stash branch <branch_name> # 从储藏创建一个分支

12. 远程操作

远程仓库是指托管在因特网或其他网络中的你的项目的版本库。

git remote # 查看已配置的远程仓库服务器

git remote -v # 指定选项 -v,会显示需要读写远程仓库使用的 Git 保存的简写与其对应的 URL。

git remote show <remote-name> # 查看制定远程仓库的更多信息

git remote add <shortname> <url> # 添加一个新的远程 Git 仓库

git remote rm <name> # 移除远程仓库

git remote rename <oldname> <newname># 重命名远程仓库

git fetch <remote-name> # 从远程仓库中拉取数据

# 下载代码及快速合并, 当你想拉取origin服务器上的当前分支名的代码时,可简写git pull
git pull <remote-name> <branch-name>

# 第一次推送到远程仓库,--set-upstream可简写为-u
git push --set-upstream <remote-name> <branch-name>

# 推送到远程仓库, 当你想将当前开发分支名推送到 origin 服务器,可简写为git push
git push <remote> <branch-name>

git push [remote] --force # 强行推送当前分支到远程仓库,即使有冲突

git push [remote] --all # 推送所有分支到远程仓库

git push <remote> :<branch-name/tag-name> # 删除远程分支或标签

git push --tag # 上传所有标签

13. 冲突

# 一次打开所有冲突的文件,重新绑定可能会导致冲突,以下命令将打开需要您帮助解决这些冲突的所有文件。
git diff --name-only --diff-filter=U | uniq  | xargs $EDITOR

从本地初始化到上传到GitHub远程仓库的一般步骤

git init
git add .
git commit -m 'hint message'
git remote add origin 'your project repositories href on github'
git pull origin master
git push -u origin master

首次push远程仓库提交错误 -> 错误截图解决详见

error: failed to push some refs to 'https://github.com/xxx.git'
# --allow-unrelated-histories 合并了两个不相关的项目的历史记录。
git pull origin master --allow-unrelated-histories

git push -u origin master

git_commands

参考资料

Git Document中文文档

15-git-commands-you-may-not-know

阮一峰常用Git 命令清单

廖雪峰 Git教程

Runoob Git教程

Git入门

Github, 知乎,掘金,素质三连击~~

Github

知乎

掘金

查看原文

赞 44 收藏 38 评论 2

RainyCG 发布了文章 · 2019-04-16

Vue之网易云音乐PC版轮播图的实现

Github - program-learning-lists
最近在刷网易云音乐歌单时发现首页的轮播图很有意思,正好自己想尝试做一个PC版的网易云音乐,于是就是使用Vue去做这个demo,废话少说,我要出招了,接招吧

网易云音乐PC版轮播图

页面的DOM结构

<template>
  <div class="slider-container" ref='slider'
       :style="sliderStyle"
       @mouseover="pause()"
       @mouseout="play()">
    <div class="slider-content" :class="mask ? 'mask' : ''">
      <div class="slider" v-for="(item, index) in list"
        :key="index"
        :class="setClass(index)"
        @click="onClick(index)" :style="setBGImg(item.src)">
      </div>
      <i v-show="arrow" class="iconfont icon-left" @click="prev()"></i>
      <i v-show="arrow" class="iconfont icon-right" @click="next()"></i>
    </div>
    <div class="dots" v-if="dots">
      <span v-for="(item, index) in list" :key="index"
        :style="setActiveDot(index)"
        @mouseover="currentIndex = index"></span>
    </div>
  </div>
</template>

Slider-container的样式(Stylus)

.slider-container
  width: 100%
  height: 100%
  text-align: center
  padding: 10px 0
  position: relative

这个子组件主要分为两块。
第一块、轮播图,其中它们的业务逻辑是

  • 自动切换
  • 左右icon切换轮播图
  • 点击前后轮播图切换轮播图
  • 鼠标滑动到轮播图停止轮播,离开后继续轮播

Slider-content的DOM结构

<div class="slider-content" :class="mask ? 'mask' : ''">
  <div class="slider" v-for="(item, index) in list"
    :key="index"
    :class="setClass(index)"
    @click="onClick(index)" :style="setBGImg(item.src)">
  </div>
  <i v-show="arrow" class="iconfont icon-left" @click="prev()"></i>
  <i v-show="arrow" class="iconfont icon-right" @click="next()"></i>
</div>

Slider-content的样式(Stylus)

.slider-content
    position: relative
    width: 100%
    height: calc(100% - 20px)
    left: 0%
    top: 0%
    margin: 0px
    padding: 0px
    background-size: inherit
    .slider 
      position: absolute
      margin: 0
      padding: 0
      top: 0
      left: 50%
      width: 65%
      height: 100%
      transition: 500ms all ease-in-out
      background-color: #fff
      background-repeat: no-repeat
      background-position: center
      background-size: inherit
      transform: translate3d(-50%,0,-80px)
      z-index: 1
      &:before
        position: absolute
        content: ""
        width: 100%
        height: 100%
        top: 0
        left: 0
        background-color: rgba(0, 0, 0, 0)
        transition-delay: 100ms!important
        transition: all 500ms
        cursor: pointer
      &.active
        transform: translate3d(-50%, 0, 0)
        z-index: 20
      &.prev
        transform: translate3d(-75%, 0, -100px)
        z-index: 19
      &.next
        transform: translate3d(-25%, 0, -100px)
        z-index: 18
    i
      width: 17.5%
      display: none
      position: absolute
      top: 40%
      font-size: 22px
      color: rgba(255, 255, 255, 0.5)
      text-shadow: 0 0 24px rgba(0, 0, 0, 0.3)
      cursor: pointer
      z-index: 21
      &:first-child
        left: 0
      &:last-child
        right: 0
    &:hover
      i
        color: rgba(255, 255, 255, 0.8)
        display: block
    &.mask
      .slider 
        &.prev, &.next
          &:before
            background-color: rgba(0, 0, 0, 0.50)

第二块、底部的dot, 其中它们的业务逻辑是

  • 当前轮播图对应位置的dot高亮
  • 鼠标移动到相应的dot上切换对应位置的轮播图

Dots的DOM结构

<div class="dots" v-if="dots">
  <span v-for="(item, index) in list" :key="index"
    :style="setActiveDot(index)"
    @mouseover="currentIndex = index"></span>
</div>

Dots的样式(Stylus)

.dots 
  width: 100%
  height: 20px
  span
    display: inline-block
    width: 20px
    height: 2px
    margin: 1px 3px
    cursor: pointer

上面是页面的DOM结构和表现的实现代码,接下来我们要讲的是连招的实现,小心啦,我要摸眼W + R3了。
上面我们讲到轮播图的业务逻辑,接下来,我们就讲讲如何实现的的吧

自动轮播

自动轮播

play () {
  this.pause();
  if (this.autoPlay) {
    this.timer = setInterval(()=>{
      this.next();
    }, this.interval)
  }
}

暂停轮播

暂停轮播

pause () {
  clearInterval(this.timer);
}

Icon切换轮播图

Icon切换轮播图

next () {
  this.currentIndex = ++this.currentIndex % this.list.length;
},
prev () {
  this.currentIndex = this.currentIndex === 0 ? this.list.length - 1 : this.currentIndex - 1;
},

前后轮播图的切换轮播图

前后轮播图的切换轮播图

onClick (i) {
  if (i === this.currentIndex){
    this.$emit('sliderClick', i);
  } else {
    let currentClickClassName = this.sliderDomList[i].className.split(' ')[1]
    console.log(currentClickClassName)
    if (currentClickClassName === 'next') {
      this.next()
    } else {
      this.prev()
    }
  }
}

dots轮播图的切换轮播图

dots轮播图的切换轮播图

这里比较简单,只需要设置它的鼠标事件即可

@mouseover="currentIndex = index"

代码传送门:Vue网易云音乐轮播图的实现

知乎

个人博客

Github

查看原文

赞 8 收藏 5 评论 1

RainyCG 发布了文章 · 2019-04-16

Vue之网易云音乐PC版轮播图的实现

Github - program-learning-lists
最近在刷网易云音乐歌单时发现首页的轮播图很有意思,正好自己想尝试做一个PC版的网易云音乐,于是就是使用Vue去做这个demo,废话少说,我要出招了,接招吧

网易云音乐PC版轮播图

页面的DOM结构

<template>
  <div class="slider-container" ref='slider'
       :style="sliderStyle"
       @mouseover="pause()"
       @mouseout="play()">
    <div class="slider-content" :class="mask ? 'mask' : ''">
      <div class="slider" v-for="(item, index) in list"
        :key="index"
        :class="setClass(index)"
        @click="onClick(index)" :style="setBGImg(item.src)">
      </div>
      <i v-show="arrow" class="iconfont icon-left" @click="prev()"></i>
      <i v-show="arrow" class="iconfont icon-right" @click="next()"></i>
    </div>
    <div class="dots" v-if="dots">
      <span v-for="(item, index) in list" :key="index"
        :style="setActiveDot(index)"
        @mouseover="currentIndex = index"></span>
    </div>
  </div>
</template>

Slider-container的样式(Stylus)

.slider-container
  width: 100%
  height: 100%
  text-align: center
  padding: 10px 0
  position: relative

这个子组件主要分为两块。
第一块、轮播图,其中它们的业务逻辑是

  • 自动切换
  • 左右icon切换轮播图
  • 点击前后轮播图切换轮播图
  • 鼠标滑动到轮播图停止轮播,离开后继续轮播

Slider-content的DOM结构

<div class="slider-content" :class="mask ? 'mask' : ''">
  <div class="slider" v-for="(item, index) in list"
    :key="index"
    :class="setClass(index)"
    @click="onClick(index)" :style="setBGImg(item.src)">
  </div>
  <i v-show="arrow" class="iconfont icon-left" @click="prev()"></i>
  <i v-show="arrow" class="iconfont icon-right" @click="next()"></i>
</div>

Slider-content的样式(Stylus)

.slider-content
    position: relative
    width: 100%
    height: calc(100% - 20px)
    left: 0%
    top: 0%
    margin: 0px
    padding: 0px
    background-size: inherit
    .slider 
      position: absolute
      margin: 0
      padding: 0
      top: 0
      left: 50%
      width: 65%
      height: 100%
      transition: 500ms all ease-in-out
      background-color: #fff
      background-repeat: no-repeat
      background-position: center
      background-size: inherit
      transform: translate3d(-50%,0,-80px)
      z-index: 1
      &:before
        position: absolute
        content: ""
        width: 100%
        height: 100%
        top: 0
        left: 0
        background-color: rgba(0, 0, 0, 0)
        transition-delay: 100ms!important
        transition: all 500ms
        cursor: pointer
      &.active
        transform: translate3d(-50%, 0, 0)
        z-index: 20
      &.prev
        transform: translate3d(-75%, 0, -100px)
        z-index: 19
      &.next
        transform: translate3d(-25%, 0, -100px)
        z-index: 18
    i
      width: 17.5%
      display: none
      position: absolute
      top: 40%
      font-size: 22px
      color: rgba(255, 255, 255, 0.5)
      text-shadow: 0 0 24px rgba(0, 0, 0, 0.3)
      cursor: pointer
      z-index: 21
      &:first-child
        left: 0
      &:last-child
        right: 0
    &:hover
      i
        color: rgba(255, 255, 255, 0.8)
        display: block
    &.mask
      .slider 
        &.prev, &.next
          &:before
            background-color: rgba(0, 0, 0, 0.50)

第二块、底部的dot, 其中它们的业务逻辑是

  • 当前轮播图对应位置的dot高亮
  • 鼠标移动到相应的dot上切换对应位置的轮播图

Dots的DOM结构

<div class="dots" v-if="dots">
  <span v-for="(item, index) in list" :key="index"
    :style="setActiveDot(index)"
    @mouseover="currentIndex = index"></span>
</div>

Dots的样式(Stylus)

.dots 
  width: 100%
  height: 20px
  span
    display: inline-block
    width: 20px
    height: 2px
    margin: 1px 3px
    cursor: pointer

上面是页面的DOM结构和表现的实现代码,接下来我们要讲的是连招的实现,小心啦,我要摸眼W + R3了。
上面我们讲到轮播图的业务逻辑,接下来,我们就讲讲如何实现的的吧

自动轮播

自动轮播

play () {
  this.pause();
  if (this.autoPlay) {
    this.timer = setInterval(()=>{
      this.next();
    }, this.interval)
  }
}

暂停轮播

暂停轮播

pause () {
  clearInterval(this.timer);
}

Icon切换轮播图

Icon切换轮播图

next () {
  this.currentIndex = ++this.currentIndex % this.list.length;
},
prev () {
  this.currentIndex = this.currentIndex === 0 ? this.list.length - 1 : this.currentIndex - 1;
},

前后轮播图的切换轮播图

前后轮播图的切换轮播图

onClick (i) {
  if (i === this.currentIndex){
    this.$emit('sliderClick', i);
  } else {
    let currentClickClassName = this.sliderDomList[i].className.split(' ')[1]
    console.log(currentClickClassName)
    if (currentClickClassName === 'next') {
      this.next()
    } else {
      this.prev()
    }
  }
}

dots轮播图的切换轮播图

dots轮播图的切换轮播图

这里比较简单,只需要设置它的鼠标事件即可

@mouseover="currentIndex = index"

代码传送门:Vue网易云音乐轮播图的实现

知乎

个人博客

Github

查看原文

赞 8 收藏 5 评论 1

RainyCG 发布了文章 · 2019-03-27

前端学习之路之资源篇

不知不觉毕业已经快一年了,做前端也快一年了,收集的各种资料充满了我的收藏,作为一个强迫症患者,我不得不把他们好好整理,那看得叫一个清爽啊,既然整理出来了,就献给那些有需要的盆友吧,大家共勉!!

详情请移步我的笔记仓库, 这个我会一直更新来着。

React

reactjs

React - Antd

react-antd

UxCore

uxcore

ZanUI: PC、移动、小程序

zanui

React.part: 查找React的组件

react-parts

Vue

vuejs

Vue - Antd

vue-antd

IView: 一套基于 Vue.js 的高质量

iview

Element: Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库

element

Mint UI: 基于 Vue.js 的移动端组件库

Mint-UI

VUX: 一个凑合的 Vue.js 移动端 UI 组件库

vux

Vue-Map: 基于 Vue 2.x 和高德地图的地图组件

文档

vue-amap

Nodejs

Express: 高度包容、快速而极简的 Node.js Web 框架

express

koa

koa

egg: 为企业级框架和应用而生

egg

Nodejs学习笔记

Javascript

现代 Javascript 教程: 从基础知识到高阶主题,只需既简单又详细的解释。

js-info

Philip Roberts: Visualizing the javascript runtime at runtime

Github -> Demo

philip-roberts

Lodash

lodash

Ramda

ramda

Underscore

Github

underscorejs

30 seconds of code

Github

30secondsofcode

AST Explorer

Mock

Easy-Mock: 高效伪造数据

easy-mock

Mock.js:生成随机数据,拦截 Ajax 请求

mock-js

Rapid-Api

rapidapi

动画库

Animate.css

animate-css

Animejs

animejs

TweenMax.js

tweenmaxjs

GreenSock

greensock

测试框架

Mocha

Mocha GitHub

mocha

Chai

Chai Github

chai

Jest

Jest Github

jest

jsPerf — JavaScript performance playground

GitHub

优秀项目 & 插件

Webpack

webpack

Webpack config tool: webpack 配置工具

createappdev

BootCDN: 稳定、快速、免费的前端开源项目 CDN 加速服务

bootcdn

BootStrap

bootstrap

BootsWatch: Free themes for Bootstrap

bootswatch

RxJS: 使用 Observables 的响应式编程的库,它使编写异步或基于回调的代码更容易。

Github

中文文档

rxjs

Layui

Github

layui

开发资源

Awesomes前端开发资源

awesomes-web-resource

算法学习 & 机器学习

Rappid算法学习

rappid-ast

机器深度学习

depp-ast-redstone

VisuAlgo - 数据结构和算法动态可视化 (Chinese)

visualgo

Algorithm Visualizer

Github

algorithm-visualizer

Papers With Code : the latest in machine learning

paperswithcode

[Data Structure Visualizations]https://www.cs.usfca.edu/~gal...: 旧金山大学CS Data Structure

visualization

BestofML: 收集汇总了机器学习相关的资源,包括书籍、课程、博客、论文等

Github

bestofml

数学知识学习

微积分

线性代数

概率论

最优化方法

Math ∩ Programming

Math ∩ Programming

Immersive Linear Algebra: 一本会动的线代书,O(∩_∩)O哈哈~

linear-algebra

机器学习的数学基础知识

Github

Download

Linux

Linux命令大全

linuxde

Iodide: Mozilla 支持的在 Web 中实现各种数据科学的效果

Github

Iodide

Icon & 设计 & 网页

Iconfont

iconfont

FontAwesome

fontawesome

Ionicons

ionicons

Icomoon

icomoon

Mobiriseicons

mobiriseicons

zwicon

zwicon

unDraw

undraw

优设

uiiiuiii

Can I Use: 查询浏览器的特性支持情况

caniuse

Package Different

查询 NodeJS 的 ES2018 特性支持情况

开发社区 & 学习社区

Vue.js 社区

vue-js-club

React.js社区

react-china

掘金

juejin

InfoQ: InfoQ 是一个实践驱动的社区资讯站点,致力于促进软件开发领域知识与创新的传播。

w3cplus

V2EX

大前端

开源中国

segmentfault

best-chinese-front-end-blogs: 收集优质的中文前端博客

softnshare

路径及文章

众成翻译

Fly63前端

博客 & 团队

阮一峰ES6入门

廖雪峰官网

AlloyTeam - 腾讯Web前端团队

凹凸实验室

淘宝前端团队FED

奇舞团

腾讯互娱

路径

程序员不能错过的28份技术知识图谱,你的进阶路上必备

学好机器学习需要哪些数学知识?

浏览器的工作原理

工具

Codesanbox

codesandbox

Codepen

codepen

Repl.it

repl-it

Glitch

glitch

知乎

个人博客

Github

查看原文

赞 7 收藏 5 评论 0

认证与成就

  • 获得 207 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • Web-Study

    前端工作学习过程中的总结, 其他笔记: https://rain120.github.io/study-notes/

注册于 2017-08-17
个人主页被 1.1k 人浏览