Skip to content

Latest commit

 

History

History
415 lines (353 loc) · 25.9 KB

项目相关.md

File metadata and controls

415 lines (353 loc) · 25.9 KB

Blog 项目相关

ps: 其中的很多只是我在项目中遇到的一些问题和思考,有一部分还有待深入理解,如果有不对的地方或者是疑问,欢迎在issue中提出,感谢!

react

react-router history属性与区别

history属性用来监听浏览器地址栏的变换,并将URL解析成一个对象,供React-Router匹配 history对象有三个属性browserHistory,hashHistory,creacteMemoryHistory

  • hashHistory路由将通过URL的#切换(通过hash完成)
  • browserHistory 不通过Hash完成,而显示正常的路径,是会向server发送请求的,所以要对server端做特殊配置
  • createMemoryHistory主要用于服务端渲染,他创建一个内存中的history对象,不与浏览器互动。

在react引用图片的问题

webpack中不太能处理html中引用图片

  • 我们可以用es6的import方法(在webpack中一切皆模块)
  • 也可以作为css的背景引入进去,这里有点不一样的地方在,如果样式是嵌入在jsx中的那么该图片的路径应该相对于bundle文件的位置,而写在样式表中则是相对于本身(当在render中的HTML的标签里设置内联样式:background-image属性,它的url应该指向bundle文件)
  • 用src时可以使用require引入

react中使用ES6的坑

  • 类名(组件名)一定要用大写开头,否则自定义的组件无法编译,识别不出来。
  • 类中定义render函数要注意两点
    • 开头花括号一定要和小括号隔一个空格
  • 标签的前一半一定要和return一行(render如果只跟一个标签(不加花括号),必须把开头标签(比如
    )放在和return一行)
  • render中如果使用setState(浏览器前进后退只会经历render周期),会导致无限render
  • 在class中使用的变量或者方法,一定要加this
  • es6绑定事件需要bind(this) ,这样function和bind里面的参数'this'才绑定到一起.注:在原来的React.creatClass中,在事件处理柄触发的时候会自动绑定到响应的元素上面去,这时候该函数的this的上下文就是该实例,不过在ES6的class写法中,它(facebook)取消了自动绑定(也就是会自动绑定到支撑实例上--backing instance而非实例,关于支撑实例可以看知乎上这个提问虽然还没人回答。。。

关于组件化的认识

  • 在react中所谓组件就是状态机。当组件处于某个状态时那么就输出这个状态对应的页面。像纯函数一样,容易去保证界面的一致性。
  • 将项目拆分成比较小的粒度去组件化,可以提高代码的重用性。
  • 采用的是分治的思想,耦合性低,易于管理。
  • 在项目中可以将组件细分成含有抽象数据的容器组件,和只有业务逻辑的展示型组件。

子组件向父组件传递状态

  • 使用redux、flux
  • 使用回调函数来向父组件,父组件定义事件处理函数,在函数中改变父组件的state,将该函数通过props传递给子组件,然后绑定。等该事件触发时便会改变父元素的状态。
  • 可以通过使用context(就相当于一个全局变量),context可以跨级从父组件向子组件传值,也可以实现子获取和设置父暴露出来的属性值

flux

为了解决MVC数据流混乱的问题flux被提出,它的核心思想就是数据和逻辑永远单项流动。数据从action到dispatcher,再到store,最终到view的路线是单向不可逆的。dispatcher定义了严格的规则来限定我们对数据的修改操作;store中不能暴露setter的设定也强化了输一局修改的纯洁性,保证了store的数据确定唯一的状态。其中flux有三大部分构成:

  • dispatcher,负责分发事件,它有两个方法,一个注册监听器,另外一个分发一个action。注意,在flux中只有分发一个action才能修改数据,没有其他方法.
  • store,负责保存数据,同时响应并更新数据。当我们使用dispatcher分发一个action时,store注册的监听器就会被调用
  • view,负责订阅store中的数据,并且使用这些数据渲染响应的页面(使用react作为 view)

在这个结构中类似MVC但是不存在一个controller,但是却像是存在一个controller-view。主要进行store与react组件(view)之间的绑定,定义数据已经更新传递方式,它会调用store的getter获取其中的数据并设置为自己的state,然后调用setState.

  • redux 和 flux 的区别

    在flux中我们在actionCreator里面调用AppDispatcher.dispath方法来触发action,这样不仅有冗余而且直接修改了store中的数据,将无法保存数据前后变化的状态。在react中采用纯函数reducer来修改状态。

react禁止“事件冒泡”失效

因为react原生事件系统本身就是事件委托,说明事件会一直冒泡到document进行绑定。

  • React为了提高效率,把事件委托给了document,所以e.stopPropagation()并非是不能阻止冒泡,而是等他阻止冒泡的时侯,事件已经传递给document了,没东西可阻止了。
  • e.stopPropagation()不行,浏览器支持一个好东西,e.stopImmediatePropagation() 他不光阻止冒泡,还能阻止在当前事件触发元素上,触发其它事件。这样即使你都绑定到document上也阻止不了我了吧。
  • 这样做还不行,React对原生事件封装,提供了很多好东西,但也省略了某些特性。e.stopImmediatePropagation() 就是被省略的部分,然而,他给了开口:e.nativeEvent ,从原生的事件对象里找到stopImmediatePropagation(),完活。

结果: e.nativeEvent.stopImmediatePropagation() 可以完美实现预期。

react的性能优化

首先进行性能分析

React提供了性能分析插件React.addons.Perf,它让我们可以在需要检测的代码其实为期分别添加Perf.start()和Perf.stop(),并可以通过Perf.printInclusive()方法打印花费时间,然后我们可以结合数据做进一步分析。

关注shouldComponentUpdate

当我们最初使用React的时候我们天真的以为React会智能地帮我们在Props与State没有改变的时候取消重渲染,不过事实证明只要你调用了setState或传入了不同的Props,React组件就会重新渲染,不过这种方式也会存在一些缺陷:

  • 它并没有深层的比较两个对象,如果它进行了深层的比较那么该操作也会变得十分的缓慢。
  • 如果传入的Props是某个回调函数(其实引用类型都会有该隐患),那么该函数会一直返回Ture
  • 比较检测本身也是有性能损耗的,应用中过多冗余的比较反而会降低性能。

在这里我们总结一下,建议的重载shouldComponentUpdate应该适用于以下类型的组件:

  • 使用简单Props的纯组件
  • 叶子组件或者在组件树中较深位置的组件
借助react key表示组件

在这之前我们可能需要先了解一下diff算法的的相关知识,看这里,当我们的组件有自己的key的话对diff算法是友好的,通过key标识我们可以组件如:顺序改变、不必要的自组建跟新等情况下告诉react避免不必要的渲染而避免性能的浪费

关注同构

见下面node部分,会对性能大幅度提升(针对加载时间)

最后

关于js,css,html,http的其他所有优化都有用,同样,下面利用webpack的优化值得关注。

关于diff算法

在react中存在虚拟dom,当我们需要更新的时候我们会先对比dom的区别,这里就是采用了diff算法进行一个对比,如果不需要更新就不操作dom(减少dom的操作)。下面谈谈diff算法:

  • 不同类型结点的类型比较;在react中比较两个虚拟dom结点,当两个结点不同时应当如何处理分为两种情况,一种就是结点类型不同,一种就是结点类型相同但属性不同。当结点类型不同时diff算法将不比较两个结点而是将原结点删除生成一个新的结点(我们可以得出结论,DOM diff算法只会对树进行逐层的比较)。
  • 逐层进行结点比较,上面说到,diff算法只会对同一层的结点进行比较(此时我们将dom看为树,本来其实也是树。。),假设下面的情况:树的左枝A需要移动到右子树下面的我们直观的操作会是
A.parent.remove(A); 
D.append(A);

而diff算法会这样做(因为对于不同层的结点只有简单的创建和删除)

A.destroy();
A = new A();
A.append(new B());
A.append(new C());
D.append(A);
  • 相同类型结点比较,当比较的是同类型节点时,算法就相对简单容易理解,React会对属性进行重设从而实现结点的转换,需要注意的是虚DOM的style属性稍有不同,其值并不是一个简单的字符串,而必须为一个对象,因此转换过程如下
      renderA: <div style={{color: 'red'}} />
    renderB: <div style={{fontWeight: 'bold'}} />
     => [removeStyle color], [addStyle font-weight 'bold']
    
  • 列表结点的比较,上面说过在不同层的结点即使完全一样也会销毁重建,那么在同一层的结点如果我们没有key的标识React无法识别每一个结点,那么更新效率会很低,而有了key之后他们会相互匹配,然后只需要将新的结点。对于列表节点提供唯一的key属性可以帮助React定位到正确的节点进行比较,从而大幅减少DOM操作次数,提高了性能。

Node.js

关于异步I/O

  • 异步调用将请求对象放入线程池
  • 线程池寻找可用的线程,执行异步调用放入的请求对象,然后将执行完成的结果放在请求对象中,通知IOCP完成 (window下面的引擎,管理线程池)
  • 事件模型(Event Loop)完成的I/O交给I/o观察者执行回调函数。

用node同构解决SPA的SEO优化和首屏加载缓慢问题

同构的意思和i是同时在服务端和客户端渲染页面。因为SPA的内容由js产生所以它不能够被爬虫爬到,所以存在SEO的问题同构JS通常是通过Nodejs实现。在node中通过require将组件引入,服务端通过模板引擎渲染。第一次的页面加载会很快因为渲染发生在服务端

  • React在客户端上通过ReactDOM的Render方法渲染到页面
  • 服务端上,React提供另外两个方法ReactDOMServer.renderToString和ReactDOMServer.renderToStaticMarkup 可将其渲染为 HTML 字符串.注:也许你会奇怪,为何会存在两个用于服务器渲染的函数,其实这两个函数是有区别的:1. renderToString:将React Component转化为HTML字符串,生成的HTML的DOM会带有额外的属性:各个DOM会有data-react-id,第一个DOM会有data-checksum属性(这个值使用adler32算法算出,如果两个组件有相同的props和DOM结构则这个值会相同,所以这个值会给客户端判断是否要重新渲染)。2.renderToStaticMarkup:同样是将React Component转化为HTML字符串,但是生成的DOM不会有额外的属性,从而节省HTML字符串的大小。
    // 这里附上 alloy team 给的一个例子
    var express = require('express');
    var app = express();
     
    var React = require('react'),
     ReactDOMServer = require('react-dom/server');
     
    var App = React.createFactory(require('./App'));
     
    app.get('/', function(req, res) {
     var html = ReactDOMServer.renderToStaticMarkup(
     React.DOM.body(
     null,
     React.DOM.div({id: 'root',
     dangerouslySetInnerHTML: {
     __html: ReactDOMServer.renderToStaticMarkup(App())
     }
     })
     )
     );
     
     res.end(html);
    });
     
    app.listen(3000, function() {
     console.log('running on port ' + 3000);
    });
    

只有部分DOM的更新在浏览器完成。

同构的关键要素
  • DOM的一致性,在前后端渲染相同的Compponent,将输出一致的 Dom 结构。
  • 不同的生命周期,在服务端上 Component 生命周期只会到componentWillMount,客户端则是完整的。
  • 客户端 render时机,同构时,服务端结合数据将 Component 渲染成完整的HTML字符串并将数据状态返回给客户端,客户端会判断是否可以直接使用或需要重新挂载。

使用node解决跨域的问题

在项目中遇到这样一个问题,我这里有一个36kr的接口,但是这个接口是未设置cors的,同时后台的数据是XML格式,这意味着我们无法使用jsonp。这里我们可以使用node的请求转发代理来解决这个问题

var request = require('request');
//使用例子
// proxy(app,'/api','http://***');
//app是express中的app,route是本地api接口路径,remoteUrl是被代理的提供JSON数据的地址
function proxy(app,route,remoteUrl){
    app.use(route,function(req,res){//使用这个路径,即当浏览器请求route时所做的响应
      req.pipe(request(url)).pipe(res);//请求重新组合后的网址,再把请求结果返回给本地浏览器
    });
}
exports.setProxy = proxy;//导出函数

// 用于转发请求
proxy.setProxy(app,'/api/weather','http://op.juhe.cn/onebox/weather/query');
http.createServer(app).listen(port);
express中app.all和app.use的区别

首先app.use可以不挂载路径,此时应用的每个请求它都会处理。当它挂载路径的时候看似和app.all没有什么区别,其实all执行完整匹配,use只匹配前缀,例如:

app.use('/a', function (req, res) {});
app.all('/a', function (req, res) {});
// 当访问/a的时候use和all都会呗调用;但是访问/a/b的时候只有use被调用

express内置中间件

从4.X开始,Express不再依赖Connect了。除了express.static,express以前内置的中间件现在已经全部单独作为模块安装使用了。也就是说express.static是唯一内置中间件,它基于server-static,负责在Express应用中托管静态资源

express3 到 4的变化

  • 对Express内核和中间件系统的改进:不再依赖Connect和内置的中间件,需要自己添加中间件,从内核中移除了express.static外的所有内置中间件。也就是说Express是一个独立的路由和中间件web框架。关于中间件可以看这里
  • 路由系统的改变:应用现在隐式的加载路由中间件,因此不需要担心涉及到router中间件对路由中间件加载顺序的问题了,现在有两个新方法可以帮我们组织路由
    • app.route(),可以为路由路径创建链式路由语句柄
    app.route('/book')
    .get(function(req, res) {
    res.send('Get a random book');
    })
    .post(function(req, res) {
    res.send('Add a book');
    })
    .put(function(req, res) {
    res.send('Update the book');
    });
    
    • express.Router,可以创建可挂载的模块化路由语句柄
    var express = require('express');
    var router = express.Router();
    
    // 特针对于该路由的中间件
    router.use(function timeLog(req, res, next) {
      console.log('Time: ', Date.now());
      next();
    });
    // 定义网站主页的路由
    router.get('/', function(req, res) {
      res.send('Birds home page');
    });
    // 定义 about 页面的路由
    router.get('/about', function(req, res) {
      res.send('About birds');
    });
    
    module.exports = router;
    然后,在应用中加载该路由:
    
    var birds = require('./birds');
    ...
    app.use('/birds', birds);
    
  • 其他一些变化:(虽然不大但是非常重要)
    • http模块不是必须的了,除非直接使用socket.io/SPDY/HTTPS。现在可以通过app.listen()起服务
    • 已经删除了app.configure(),使用app.get('dev')检测环境并配置应用
    • req.params,从数组变为对象
    • res.locals,从函数变为对象

ES6

模块化的优缺点

优点
  • 可维护性
    • 灵活架构,焦点分离
    • 模块间组合、分解(重用)
    • 方便单个模块调试、升级
    • 多人协作互不干扰
  • 可测试性
    • 可分单元测试
缺点
  • 性能损耗
    • 系统分层,调用链会很长
    • 模块间通信,模块间发送消息会很耗性能
  • 内聚度:内聚度指模块内部实现,它是信息隐藏和局部化概念的自然扩展,它标志着一个模块内部各成分彼此结合的紧密程度。
  • 耦合度:耦合度则是指模块之间的关联程度的度量。耦合度取决于模块之间接口的复杂性,与模块相反。

模块化

在该项目中利用的是es6中的模块化,利用webpack构建,它支持多种模块化的方案,es6模块化与其他方案的不同:

  • es6模块化的设计思想是尽量静态化,是的编译时就能确定模块的依赖关系,以及输入输出的变量。CommonJs和AMD模块都只能在运行时确定这些东西。例如
    // conmmonjs
    let {stat,exists} = require('fs');
    // 代码运行的实质是整体加载fs模块(既加载fs的所有方法,这称为运行时加载)
    
    // es6
    import {state,exits} form 'fs';
    // 以上代码的实质是从fs模块加载两个方法,其他方法并不加载。这成为“编译时加载”
    
    es6在编译时就可以完成模块的编译,效率要比commonjs的加载要高。
  • es6自动在模块中采用严格模式
  • es6模块化加载机制与Commonjs完全不同,CommonJs模块输出的是一个值的拷贝,而es6输出的则是值的引用。

模块化之间的对比

  • CommonJs:一个单独的文件就是一个模块,每一个模块都是一个单独的作用域,也就是说在该模块内部定义的变量无法被其他模块读取,除非定义为global对象。加载模块使用require方法,该方法读取一个文件并执行,返回文件内部的module.exports对象。但是,由于在commonJs规范中,require的代码之后的内容必须加载完成后才可以继续运行,使得Coomonjs规范不适用于浏览器环境(常用于服务器环境,比如node,因为在这些环境中i/o速度要快)。
  • AMD:上面说到commonjs环境不适合浏览器环境,而AMD就解决了这个问题,它是以异步的方式加载模块,模块的加载不影响后面的语句运行,对于依赖的模块提前执行,依赖前置。require.js就是AMD规范。
define(['dep1','dep2'],function(dep1,dep2){
     //内部只能使用制定的模块
      return function(){};
});
  • CMD:它与AMD的区别就是在定义模块的时候它不需要立即声明依赖,在需要的时候require就可以了。
  • 关于es6的请看上面一个话题
关于模块化的循环加载

ps:这里只谈谈commonjs和es6中的解决方案。假设问题的场景是a脚本的执行依赖b,而b脚本的执行又执行a。

  • commonjs:commonjs的加载原理是当require命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成一个对象。也就是说即使再次执行require命令,也不会再次执行,而是到缓存中取值。所以,在出现上面循环加载的情况时,就只输出已经执行的部分,还未执行的部分不会输出。
  • es6处理循环加载与commonjs有本质不同。es6是动态引用,需要开发者保证真正取值时能够取到值,这里提出一个疑问:在es6的循环加载中是否必须需要有跳出条件?否则会不断循环引用吗?

模块化加载的原理

  1. 模块的查找,通过id和路径的对应原则,加载器才能知道需要加载的js路径。在AMD规范里可以通过配置Paths来特定的id(模块名)配置path。
  2. 请求模块,知道路径之后就要去请求,一般通过createElement('script') && apppendChild去请求,一般来需要给<script>标签设置一个属性来标识模块id。
  3. 给定义的模块设置id
  4. 依赖分析,模块在Define之后不是马上就可以用了,在你执行factory方法来产生出最红的export之前要保证依赖是可用的
  5. 递归加载

ES6中class的一些tip

  • 同样也是函数,在定义方法时前面不需要加上function这个保留字

  • 类的所有方法都定义在类的prototype上,在类的实例上调用原型方法。

  • 除了构造函数其他方法都定义在prototype对象上,所以类的新方法可以添加在prototype上。object.assign方法可以很方便地一次向类添加多个方法

  • 类内部定义的所有方法都是不可枚举的(enumerable)

  • class不存在变量提升,也就是说在该类定义之前不允许访问它。

  • 和es5继承不同(先创造子类的实例对象this,然后再将父类的方法添加到this上),es6得到父类的this对象,然后再丰富它。所以要调用super方法取到父类的this对象

  • 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。因为子类实例的构建基于对父类实例的加工,只有super方法才能返回父类实例

  • es6中有两条继承的线路

    • 子类的_proto_属性表述构造函数的继承,总是指向父类
    • 子类prototype属性_proto_属性表述方法的继承,总是指向父类的prototype属性
  • Object.getPrototypeOf()方法可用于从子类上获取父类,可以用这个方法判断一个类是否继承了另一个类

webpack

官网对webpack的定义是模块打包工具,它的目的就是把有依赖关系的各种文件打包成一系列静态资源。

webpack的优点

  • 支持主流的模块化标准
  • 可以通过不同的loader,支持打包css、scss、json、markdown等格式文件
  • 有完善的破坏/散列系统(待学习)
  • 内置热加载功能
  • 有一系列优化方案和插件机制来满足各种需求

关于webpack的优化

由于该项目开发的是单页应用,不存在设置多个entry的做法,只能把js都build到一个bundle中(暂不考虑根据router跳转按需请求的做法),导致bundle文件非常巨大,这样导致的问题包括:

  1. 严重影响首次加载时间
  2. 每次有任何修改的地方原先的缓存都无法再用,浪费带宽。
  • 对于一些npm包我们需要的只是其中一部分的功能,同时这些包可能有很多自己单独的包可以装。
  • 善用alias,resolve中有一个alias的配置项目,能够让开发者指定一些模块引用的路径。对于一些经常要被import或者require的库我们最好可以直接指定他们的位置没这样webpack可以节省搜索时间。
  • 在loaders中的exclude属性,大胆的屏蔽掉npm里的包(因为它们大多不需要通过loader去处理),甚至在我们十分确信的情况下使用include来限定babel的使用范围,进一步提高效率。
    var path = require('path');
     module.exports = {
         module: {
             loaders: [
               {
                   test: /\.js(x)*$/,
                   loader: 'babel-loader',
                   exclude: function(path) {
                       // 路径中含有 node_modules 的就不去解析。
                       var isNpmModule = !!path.match(/node_modules/);
                       return isNpmModule;
                   },    
                  //甚至采用下面这种方法
                   include: [
                     // 只去解析运行目录下的 src 和 demo 文件夹
                     path.join(process.cwd(), './src'),
                     path.join(process.cwd(), './demo')
                   ],
                   query: {
                       presets: ['react', 'es2015-ie', 'stage-1']
                   }
               }
             ]
         },
         reslove: {
             extendsions['','.js','.jsx']
             // 这里!! 可以使用alias配置项,可以显示的指定我们常用的一些库,避免webpack自己查找,加快查找速度
         }
     }
    
  • cache,使用watch模式自动开启缓存
  • 采用Code Splitting(下面)

webpack 的 Code Splitting

对于大型的web应用而言,把所有的代码放在一个文件的做法效率很差,特别是加载了一些只有特定环境下才会使用到的阻塞代码的时候。

code Splitting的具体做法就是一个分离点,在分离点中依赖的模块会打包到一起,可以异步加载。一个分离点会产生一个打包文件。

webpack 提供了一个 require.ensure(dependencies, callback, chunkName) 方法。 第一个参数是一来,第二个是回调函数,第三个是chunkName,用来指定这个chunk file 的 name

下面是一个react-router中的实例,chunkA只有当进入rootUrl/A才会加载,chunk B 只有当进入rootUrl/B才会加载。

import RouteA from './RouteA'
import RouteB from './RouteB'

export default {
  path: '/',
  component: App,
  childRoutes: [
    RouteA,
    RouteB,
  ],
  indexRoute: {
    component: Index
  }
}

/* ---              RouteA              --- */
export default {
  path: 'A',
  getComponent(location, cb) {
    require.ensure([], (require) => {
      cb(null, require(`${PathOfRelatedComponent}`))
    }, 'chunkA')
  }
}

/* ---              RouteB              --- */
...
export default {
  path: 'B',
  getComponent(location, cb) {
    require.ensure([], (require) => {
      cb(null, require(`${PathOfRelatedComponent}`))
    }, 'chunkB')
  }
}

另外:你可能会遇到这样的错误React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components). Check the render method of RoutingContext. 在 require() 之后加一个 .default 即可。

如果你收到了这样的错误提示: require.ensure is not function , 增加一个polyfill即可: if (typeof require.ensure !== 'function') require.ensure = (d, c) => c(require) ,在Server端使用require来代替require.ensure.

重构方案

  1. 使用redux管理数据流做登陆登出的状态管理
  2. 使用webpack的code Splitting做延迟加载
  3. 重构样式
  4. 购买云服务器上线
  5. 使用node同构和直出增进性能和优化seo
  6. 使用CND缓存图片,js等静态资源