一、现代前端开发
1.1 ES6 —— 新一代的JavaScript标准
1.1.1 语法特性
- const、let关键字
- 函数(箭头函数、默认参数、rest参数)
- 展开操作符(...,函数调用,数组自变量)
- 模板字符串
- 解构赋值
- 类(class)
- 模块(import、export)
1.1.2 Babel
一种多用途的JavaScript编译器,它把最新版的JavaScript编译成当下可以执行的版本。Babel的核心概念就是利用一系列的plugin来管理编译规则,通过不同的plugin,它不仅可以编译ES6代码,还可以编译React JSX语法或者是CoffeeScript等。
1.2 前端组件化方案
模块(module)和组件(component):
模块: 语言层面,在前端领域我们说 module 一般都是指 JavaScript module,往往表现为一个单独的JS文件,对外暴露一些属性和方法。
组件: 业务层面,一个可独立使用的功能实现,往往表现为一个UI部件。一个组件包含它所需要的所有资源,包括逻辑、样式、模板,甚至图片和字体。
因而一个组件有时仅仅是一个JavaScript模块,而更多时候不仅是一个JavaScript模块。前端的组件化方案都不可避免的要以模块化方案为基础。
1.2.1 JavaScript模块化方案
在ES6之前,JavaScript并没有原生的模块,JavaScript开发者通过各种约定或妥协来实现模块的特征,这一过程大致经历了三个阶段:
1) 全局变量 + 命名空间
基于同一个全局变量,各模块按照各自的命名空间进行挂载。
//定义
const foo = window.foo;
const bar = `i\'m bar`;
//export
foo.bar = bar;
//使用
const foo = window.foo;
foo.bar; // `i\'m bar`;
主要问题如下:
- 依赖全局变量,污染全局作用域的同时,安全性得不到保障;
- 依赖约定命名空间来避免冲突,可靠性不高;
- 需要依赖手动管理来控制执行顺序,容易出错;
- 需要在最终上线前合并所有用到的模块。
2)AMD + CommonJS
AMD 将革命性的JavaScript模块化方案带到了前端开发中,它解决了前面方案的几乎所有问题
- 仅仅需要在全局环境下定义require与define,不需要其他的全局变量;
- 通过文件路径或模块自己声明的模块名定位模块;
- 模块实现中声明依赖,依赖的加载与执行均由加载器操作;
- 提供了打包工具自动分析依赖合并。
AMD模块一般写法如下:
define(function (require) {
//通过相对路径获得依赖模块
const bar = require('./bar');
//模块产出
return function(){
//...
}
})
CommonJS不适合浏览器环境,但依赖现代打包工具的能力,CommonJS规范的模块也可以经过转换后再浏览器中执行。相比AMD的模块格式,CommonJS的模块格式更简洁,而且可以更方面地实现前后端代码共用。
CommonJS模块一般写法如下:
//通过相对路径获取依赖模块
const bar = require('./bar');
//模块产出
module.exports = function(){
//...
}
3)ES6模块
为JavaScript世界带来了规范的模块化方案,相比AMD和CommonJS,它更为强大,引用与暴露的方式更多样。而且它支持比较复杂的静态分析,使构建工具更细粒度地移除模块实现中的无用代码成为可能。
ES6模块一般写法如下:
//通过相对路径获得模块依赖
import bar from './bar';
//模块产出
export default function(){
//...
}
1.2.2 前端的模块化和组件化
前端的组件化方案在模块化方案的基础上也经历了漫长的演变过程,大致可以分为四个阶段:
1)基于命名空间的多入口文件组件
-
基于全局变量和命名空间进行挂载
-
不同资源分别手动引入(或手动合并)
最典型的例子就是jQuery插件
2)基于模块的多入口文件组件
前端开始有了流行的模块化方案,这一时期的组件也趋向于使用AMD这样的规范来组织其JavaScript实现,把自己也暴露为一个模块。
呈现为:
- 一个AMD模块,为JavaScript实现;
- 一个CSS(或Less、Sass)文件,为样式内容;
- 其他资源内容,往往不需要手动引入,组件会在其CSS实现中通过相对路径引入。
使用时:
- 在JavaScript代码中require组件对应的模块;
- 在样式代码中引入(CSS预处理器提供的import等方式)组件的样式内容。
3)单JavaScript入口组件
browserify、webpack等现代打包工具的出现为解决了上一个方案遗留的问题带来了了曙光。它们允许我们将一般的资源视作与JavaScript平等的模块,并以一致的方式加载进来。
于是,我们组件的所有依赖都可以再自己的实现中声明,而对外值暴露一个JavaScript模块作为入口。以优雅的方式解决已有方案的问题,借助JavaScript强大的表现能力与相关工具使该组件方案拥有了极大的可扩展性,目前最受欢迎的前端组件化方案。
4)Web Component
前端组件化方案里的“国家队”,就像ES6 module对于JavaScript模块化方案一样。此方案包含四个部分:
- 自定义元素(Custom Element)
- HTML模板(HTML Templete)
- Shadow DOM
- HTML的引入(HTML Import)
拥有这四项本领的Web Component为我们构造了一个美好的愿景——像使用普通HTML标签一样使用组件,组件的样式内外隔绝,通过简单的<link rel='import' href='bar.html' />就可以引入组件实现。然而因为浏览器的支持度远远不够,没能流行开来。
1.3 辅助工具
1.3.1 包管理器(package manager)
包(package)和模块(module)的区别和联系:
包是一个用package.json文件描述的文件夹或文件;而模块的要求更为具体——模块指的是认可可以被Node.js中require方法载入的文件。下面几个可以被当作模块的典型例子:
- 一个包括有main字段package.json的文件夹
- 一个包括index.js的文件夹
- 一个独立的JavaScript文件
所有的模块都是包,但不是所有的包都是模块。
1.3.2 任务流工具(Task Runner)
在前端项目中会遇到各种各样的任务,比如说压缩合并代码、验证代码格式、测试代码、监视文件是否变化等。执行这些任务的方法一般都是在命令行中执行相应的命令。
1)Grunt
它有非常完善的插件机制,插件是把各种工具和Grunt结合在一起的桥梁。
Grunt这个工具是用插件机制和Gruntfile.js实现了多任务的配置、组合和运行,使用前端开发者熟悉的JavaScript文件比bash脚本更容易学习和接受。
2)Gulp
后起之秀,吸取了Grunt的优点,并且推出了很多全新的特性。配置比Grunt更简单,实现更清晰明了。
Task Ranner可以帮助你更加轻松地配置、管理任务,做到事半功倍。
1.3.3 模块打包工具(Bundler)
组件和模块化的发展历程,之前使用的还是全局命名空间的挂载方式,随着AMD、CommonJS、ES6的陆续出现,模块化开发有了更多新的实践,但是由于浏览器环境的特殊性,想Node.js中用require同步加载的方式无法使用。直到browserify的出现,打破了这一鸿沟。将浏览器不支持的模块进行编译、转换、合并,并且最后生成的代码可以在浏览器端良好运行的工具,我们称之为bundler。
1. browserify
先驱者,它使得浏览器端使用CommonJS的格式组织代码成为可能。
//add.js
module.export = functon(x, y){
return x + y;
}
//test.js
const add = require('./add');
console.log(1, 3); // 3
如果要在浏览器中使用,可以通过browserify来处理
browserify test.js > bundle.js
生成的bundle.js就是已经处理完毕、可供浏览器使用的文件,只需要插入到<script>
标签里面即可。
2. webpack
后起之秀,支持AMD和CommonJS类型,通过loader机制也可以使用ES6的模块格式。它通过一个config文件,还能提供更加丰富的功能,支持多种静态文件,还有强大的code splitting。
Bundler的主要任务是突破浏览器的鸿沟,将各种格式的JavaScript代码,甚至是静态文件,进行分析、压缩、合并、打包,最后生成浏览器支持的代码。
二、webpack
2.1 webpack的特点与优势
2.1.1 webpack与RequireJS、browserify
RequireJS 是一个JavaScript模块加载器,基于AMD规范实现。它同时也提供了对模块进行打包与构建的工具r.js,通过将开发时单独的匿名模块具名话进行合并,实现线上页面资源加载的性能优化。
browserify 是一个以在浏览器中使用Node.js模块为出发点的工具。它最大的特点在于以下两点:
- 对CommonJS规范(Node.js模块所采用的规范)的模块代码进行的转换与包装。
- 对很多Node.js的标准package进行了浏览器端的适配,只要是遵循CommonJS规范的JavaScript模块,即使是纯前端代码,也可以使用它进行打包。
webpack 则是一个为前端模块打包构建而生的工具。它既吸取了大量已有方案的优点和教训,也解决了很多前端开发中已存在的痛点,如代码的拆分与异步加载、对非JavaScript资源的支持等。
2.1.2 模块规范
RequireJS 项目本身是最流行的AMD规范实现。AMD通过将模块的实现代码包在匿名函数中实现作用域的隔离,通过文件路径作为天然的模块ID实现命名空间的控制,将模块的工场方法作为参数传入全局的define,使得工厂方法的执行时机可控,也就变相模拟出同步的局部require,因而AMD的模块可以不经转换地直接在浏览器中执行(不多的优点之一)。
browserify 支持的则是符合CommonJS规范的JavaScript模块。不严格的说,CommonJS可以看成去掉了define及工场方法外壳的AMD。没有define的CommonJS模块无法直接在浏览器中执行的——浏览器环境中无法实现同Node.js环境一样同步的require方法。因为没有define与工场方法,CommonJS模块书写起来更讲解、干净,所以越来越多前端项目开始采用此规范进行书写。
webpack 支持这两种模块格式,甚至支持二者混用。而且通过使用loader,webpack也可以支持ES6 module,可以说覆盖了现有所有主流的JavaScript模块化方案。
2.1.3 webpack的特色
- 代码拆分(code splitting)方案
- 智能的静态分析
- 模块热替换(Hot Module Replacement)
三、React
一个声明式、高效、灵活的、创建用户界面的JavaScript库。
声明式 是指只要使用React描述组件的样子就可以改变用户界面。
高效 主要得益于React的虚拟DOM,以及其Diff算法。
灵活 是指React可以作为视图层与其他技术栈配合使用,比如替代Angular的指令,或者与Redux搭配,等等。
三大特点:
1)组件 React的一切都是基于组件的。使用React,你唯一要关心的就是构建组件。组件有着良好的封装性,组件让代码的复用、测试和分离都变得更加简单。各个组件都有各自的状态,当状态变更时,便会重新渲染整个组件。
2)JSX 一种类似XML的写法,它可以定义类似HTML一样简洁的梳状结构。结合了JavaScript和HTML的优点,既可以像平常一样使用HTML,也可以在里面嵌入JavaScript语法。但是JSX和HTML完全不是一回事,JSX只是作为编译器,把类似HTML的结构编译成JavaScript。
3)Virtual DOM 在React设计中,开发者不太需要操作真正的DOM节点,每个React组件都是用Virtual DOM渲染的,它是一种对于HTML DOM节点的抽象描述,你可以把它看成是一种用JavaScript实现的结构,它不需要浏览器的DOM API支持,所以它在Node中也可以使用。它和DOM的一大区别就是它采用了更高效的渲染方式,组件的DOM结构映射到Virtual DOM上,当需要重新渲染组件时,React在Virtual DOM上实现了一个Diff算法,通过这个算法寻找需要变更的节点,再把里面的修改更新到实际需要修改的DOM上,这样就避免了渲染整个DOM带来的巨大成本。
React的出现允许我们以简单粗暴的方式构建我们的页面:仅仅声明数据到视图的逻辑转换,然后维护数据的变动,自动更新视图。它看起来很像每次状态更新时,都需要整体地更新一次视图,但React的抽象层避免了这一做法带来的弊端,让这一开发方式变得可行。
ReactElement与组件实例
ReactElement
- ReactElement是什么?
ReactElement是一个不可变的普通对象,它描述了一个组件的实例或一个DOM节点。它只包含组件的类型(比如h1、或者App)、属性以及子元素等信息。
ReactElement不是组件的实例,不能在ReaceElement中调用React组件的任何方法。
- ReactElement的两种类型
当ReactElement的type属性是一个字符串时,它表示了一个DOM节点,它的props属性对应了DOM节点的属性。
当ReactElement的type属性是一个表示组件的函数或类时,它表示一个组件。
两种类型的ReactElement可以相互嵌套,彼此混合来描述DOM树。
- React组件的渲染流程
当React遇到表示组件的ReactElement时,它会给这个ReactElement组件一些props(有时包括Context),然后问该组件渲染的ReactElement是什么类型。如果渲染的仍然是表示组件的ReactElement,那么会一直问下去,直到了解所有组件要渲染的DOM元素为止。
对一个组件来说,props就是输入,ReactElement就是输出。
组件实例
- 什么是组件实例?
组件实例是组件类的实例化对象,它通常被用来管理内部状态、处理生命周期函数。
无状态函数是没有实例化对象的,因此无法使用生命周期函数,也没有内部状态。所以如果你的组件需要使用生命周期函数或者内部状态,请使用类编写该组件。
- 组件、ReactElement与组件实例的区别
组件是一个函数或类,它决定了如何把数据变成视图;ReactElement只是一个普通对象,它描述了一个组件实例或DOM对象;组件实例则是组件类的实例化对象。
四、Redux
一个JavaScript状态容器,提供可预测的状态管理。
Redux可以用三条基本原则来描述:单一数据源;state只读;使用纯函数来执行修改。
单一数据源是指整个应用的state被存储在一棵对象树中,并且这个对象树只存在于唯一一个store中。
state只读并不代表我们无法改变state。而是不允许直接对state这个变量重新赋值,但是可以通过action和reducer返回一个新的state,而且只能使用这一方法。
使用纯函数来执行修改是指更新state的reducer只是一些纯函数,它接收先前的state和action,并返回新的state。
纯函数:给出同样的参数值,该函数总是求出同样的结果。结果的求值不会促使任何可语义上可观察的副作用或输出。
为什么要使用Redux?
- 可预测:Redux只有一个数据源,想要修改它只能发起action,reducer又是纯函数,相同的输入永远会得到相同的输出。这一切都使程序变得可控、可预测。
- 便于组织代码管理:严格而明确的程序结构使得代码更容易组织和管理,同时也方便了团队协作。
- 支持Universal渲染:单一数据源这个原则可以帮助解决Universal渲染中的数据传递问题,服务端渲染后只需给客户端传递一个变量即可,这个变量就是存储state的对象树。
- 优秀的拓展能力:Redux支持多种拓展方式,但主要是中间件的拓展。
使用Redux并不是为了简化代码量,而是它带来了清晰的数据流,并且合理地把数据和组件的state分离,对于比较复杂的多人项目来说,这样做保持了清晰的逻辑,数据流动更加明了,提供了可预测的状态,避免了多向数据流带来的混乱和维护困难的问题。
Action
Action本质上是JavaScript普通对象,它是store数据的唯一来源。我们约定,action内使用一个字符串类型的type字段来表示将要执行的动作。
Reducer
Reducer是个形式为 (state, action) => state 的纯函数,描述了action如何把state转变成下一个state。
Store
Store是个全局对象,将action以及reducer联系在一起。负责更新、查询、订阅state等多个工作。Store有一下职能:
- 维持应用的state。
- 提供getState()方法获取state。
- 提供dispatch(action)方法更新state。
- 通过subscribe()注册监听器。
React-Redux
Connect的多种写法
-
connect()的第一个参数是参数为state的函数,该函数返回的对象将被合并到组件的props中。connect()的第二个参数是多个action创建函数组成的对象,该对象也会被合并到组件的props中,而且可以直接运行,无需使用dispatch。
import Counter from './Counter'; import { connect } from 'react-redux'; import * as ActionCreators from './actions'; export default connect( state => ({ counter: state.counter }), ActionCreators )(Counter)
-
connect()的第一个参数不变。connect()的第二个参数是蚕食为dispatch的函数,该函数返回的对象也会被合并到组建的props中。注意,改写法不会为action创建函数绑定dispatch方法,所以需要我们手动绑定。
import Counter from './Counter'; import { connect } from 'react-redux'; import { increment, decrement} from './actions'; export default connect( state => ({ counter: state.counter }), dispatch => ({ increment: () => dispatch(increment()), decrement: () => dispatch(decrement()) }) )(counter)
-
connect()的第一个参数不变。connect()第二个参数是参数为dispatch的函数,但是函数的返回值使用了redux的bindActionCreators()来减少样板代码。
import Counter from './Counter'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import * as ActionCreators from './actions'; export default connect( state => ({ counter: state.counter }), dispatch => bindActionCreators (ActionCreators, dispatch) )(Counter)
-
connect()的第一个参数不变。connect()的第二个参数为空。此时connect()将会自动给组件传递一个dispatch,让组件自己使用dispatch来发起action创建函数。
import Counter from './Counter'; import { connect } from 'react-redux'; export default connect( state => ({ counter: state.counter }) )(Counter) *** <button onClick={() => this.props.dispatch(increment())}>Button</button> ***
-
使用装饰器写法将connect()写在组件类声明上面。和上面除了语法上的区别外,并无二致。
import { connect } from 'react-redux'; import * as ActionCreators from './actions'; @connect( state => ({ counter: state.counter }), ActionCreators ) export default class Counter extends React.Component{ }
连接原理
Provider工作原理
Provider只是一个React组件,它的职能是通过context将store传递给子组件。因为Provider组件是通过context传递store的,所以里面的组件,不管跨多少级,都可以通过connect()方法获取store并进行连接。
connect工作原理
connect是一个嵌套函数。运行connect()后,会生成一个高阶组件(Higher-order Components)。该高阶组件接受一个组件作为参数再次运行,会生成一个新组建。
网友评论