react、redux什么的都用起来 【4】生产部署和优化

如今项目已经有了,可是要把它放到生产环境中仍是有些事情要作,在这最后一节,来把它们一一搞定。javascript

这一节其实更可能是关于webpack的内容。不过要想把react用得很爽,咱们须要一个现代化的构建工具。在前面几节webpack都在默默地工做着。react全都是关于组件的,组件意味着模块化,webpack让前端模块化得淋漓尽致。咱们的目标是要把react用起来,而且是很舒坦的用起来,因此我以为这节并没跑题,并且很重要。

打包部署文件

咱们的源代码是无法直接跑起来的。ES6语法大部分浏览器还不彻底支持,有些浏览器彻底不支持。而less、sass这些样式框架就更不用说了。另外对这些代码最好进行压缩,以得到更快的访问速度。因此在正式发布这些代码前必须先要编译打包。webpack但是干这个的以大能手,看名字就知道了。那要怎么打包呢?终端执行:css

npm run dist

搞定。html

如今咱们的项目目录里多出了一个名为dist的文件夹,这里面就是要部署的所有内容。因为generator-react-webpack-redux已经为咱们作好了webpack的一些配置,因此咱们看到打包好的文件已经通过了压缩混淆。前端

服务器设置

若是咱们在使用react-router的时候选择了浏览器历史管理方式,那么服务器必需要可以正确处理各类路径。实际上咱们的应用只有一个页面文件,在访问各类有效路径的时候,服务都应该返回那惟一的页面。在开发过程当中,咱们经过npm start指令启动了一个node服务,它已经处理好了这些路由。可是在实际生产环境中,咱们每每会使用一个静态服务器,好比nginx或apache。若是把刚才打包好的dist目录扔给nginx,你会发现只有根路径能够访问,经过点击跳转到各个路由没问题(也就是经过react-router控制的跳转),要直接在浏览器的地址栏输入"http://localhost/news"这样的自路径就404了。如今以nginx为例来配置好适合咱们应用的路由。java

咱们所需配置的内容都在http > server节点下。node

首先考虑对诸如/news这样的路径并不存在对应的页面文件,因此对于未知路径要都给打发到根路径下:react

location / {
  root   /Users/someone/my-project/dist;
  index  index.html index.htm;
  try_files $uri /index.html;
}

这样,咱们在地址栏输入"http://localhost/news"之后,nginx没有找到news.html,它就尝试找index.html,inedex.html打开后,咱们的代码就生效了,react-router看到地址栏里的路径是/news,它就会在一开始去匹配/news,并改变状态。webpack

至于脚本、图片这些静态文件咱们不用处理,由于nginx按照路径就能够直接找到这些文件。另外就是把后端服务的接口处理好,nginx代理tomcat这些后端服务是很常见的配置,只要注意在路径上服务和页面要能明显区分开,好比全部的后端服务接口都有.do后缀,这样配置就好了:nginx

location ~*.do$ {
  proxy_pass   http://192.168.1.1:8088;
}

分离样式文件

尽管在示例代码里我把样式都写成内联形式的了,但我仍是建议写单独的样式文件。前面也提到过,样式文件能够直接在js代码中引入,这对于构造独立的模块很是方便。可是在默认状态下,咱们会发现导出的文件没有css文件,实际上导入的样式是在代码运行时加到页面上的style标签里的。这样页面渲染性能不太好,并且会增大js文件的体积,最好仍是把它拿出来。万能的npm里有专干这个的webpack插件,来把它装上先:web

npm install extract-text-webpack-plugin --save-dev

而后要修改一下webpack的配置文件。因为这个插件只有在打包的时候才会用到,因此咱们只改cfg/dist.js文件。引入这个插件,而后在plugins数组里添加相应的项目:

// ...
let ExtractTextPlugin = require('extract-text-webpack-plugin');
// ...
let config = _.merge({
  // ...
  plugins: [
    // ...
    new ExtractTextPlugin('app.css')
  ]
// ...

还要改一下loader。本来loader是写在cfg/base.js里面的,可是在开发环境中咱们用不到这个插件,而若是使用了插件提供的loader就会报错,因此咱们在dist.js里面把config.module.loaders数组覆盖。假如咱们的项目里用到了css和less两种样式文件,就在config.module.loaders.push这一段前面添加以下代码:

config.module.loaders = [
  {
    test: /\.css$/,
    loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
  },
  {
    test: /\.less/,
    loader: ExtractTextPlugin.extract('style-loader', 'css-loader!less-loader')
  },
  {
    test: /\.(png|jpg|gif|woff|woff2)$/,
    loader: 'url-loader?limit=8192'
  }
]

这里除了两种样式文件的loader之外,还把base里的一个非样式的loader给带过来了,别把它忽略了,它颇有用,一下子再说。

如今再运行npm run dist,能够看到asset文件夹里多了一个app.css文件。别忘了在index.html文件里面引入新生成的样式文件。

加载图片

webpack让咱们能够在js代码中引入图片并使用,引入图片只需一个简单的require语句:

let logo = require('../images/logo.png');

而后能够像使用其它变量同样来使用这个图片:

render(){
  return <img src={logo}>
}

你可能以为,一个图片直接用它的路径就好了,何须要装模做样的引入呢?我认为有这么作两个好处:

首先仍是模块化。若是一个组件须要用到图片,在这个组件文件内引入图片,图片会在run dist时一并打包,不用担忧图片丢失。

其次不少服务器会对图片进行CDN缓存,若是你替换了一张图片,极可能它在一段时间内不会生效,而经过webpack引入的图片是一内联base64或者重命名为惟一hash文件名的形式打包的,这样就不会出现恼人的缓存状况。

不仅是在js中引入图片会被webpack处理,css里的图片也会被一样的方式处理。

若是你已经在你的项目里加上了几个小图片,你可能会发现打包后并无看到图片或者图片比原来少,这是由于有一个临界值,低于它的图片会直接转成base64写在导出的js文件里。这样也好也很差,好处是图片在一开始就被载入,后面不会出现图片延后载入的效果,用户体验很好,很差就是base64比原图片大小更大,若是图片比较多,导出的js文件就会太大,让用户初始等待时间过长。因此咱们要权衡利弊设置一个合适的临界值。前面咱们在dist.js配置文件中重写loaders的时候把base里的一个loader带了过来,它就是干这个用的,test属性的正则表达式代表咱们想让webpack处理什么格式的图片,loader属性最后的数字就是内联图片临界值,单位是字节。咱们把它设置成1K吧:

{
  test: /\.(png|jpg|gif|woff|woff2)$/,
  loader: 'url-loader?limit=1024'
}

多个入口

咱们的目标是单页应用,可是当项目规模比较大的时候整个项目可能会被拆分红多个单页应用。拆分多个应用的关键在于要有多个入口文件。目前咱们的项目只有一个入口文件:src/index.js。来看cfg/dist.js文件,里面的config对象中entry属性的值如今是一个index.js路径字符串。entry的值也能够是一个对象,这样就能够声明多个入口文件,对象的key对应着文件名。好比咱们想要增长一个入口文件src/test.js,先搞点很简单的内容:

import React from 'react';
import { render } from 'react-dom';

render(
  <div>TEST</div>,
  document.getElementById('app')
);

把cfg/dist.js中的config.entry改为这样:

entry: {
  app: path.join(__dirname, '../src/index'),
  test: path.join(__dirname, '../src/test')
}

如今明确指定了两个入口文件,而后还要修改config.output.filename:

config.output.filename = '[name].js'

输出文件时,name会自动对应成entry中的key。执行npm run dist,如今asset目录中多出了个test.js。

使用这个文件须要另外一个单独的页面,若是咱们用静态html页面的话,要把页面路径添加到项目根目录下的package.json中,在scripts对象中有个copy属性,加到里面就好了,这样才能在run dist的时候把它一并拷贝到dist目录里。

最后,也许你还要修改一下nginx配置,让test路径单独匹配。

分离第三方库

你可能发现了刚才咱们把文件分红多个入口时,新入口文件即便内容很是少,哪怕只渲染了一个div,生成的文件大小还有上百k。里面其实主要都是第三方库。这太不优雅了,既然这些第三方库几乎会被全部的应用重复使用,必定得把他们单拎出来。因而咱们须要一个插件:CommonsChunkPlugin。这个插件不用单独安装了,它被包含在webpact.optimize里面。咱们打算再输出一个叫commons.js的文件,包含所有第三方库。在cfg/dist.js的plugins数组里面添加这个插件:

new webpack.optimize.CommonsChunkPlugin('commons', 'commons.js')

而后在entry对象里面再添加一个commons属性,它的值是一个数组,包含全部咱们想要拎出来的库:

entry: {
  app: path.join(__dirname, '../src/index'),
  test: path.join(__dirname, '../src/test'),
  commons: [
    'react',
    'react-dom',
    'react-redux',
    'react-router',
    'redux',
    'redux-thunk'
  ]
}

OK,输出的文件多了个commons.js,而app.js和test.js比原来小了不少。这回优雅了。别忘了在全部的页面里都把commons.js引进去。

按需加载

当项目很是大的时候,拆分多个入口文件是一种方案,还有一种方案是按需加载,也就是懒加载或异步加载。咱们可让用户真正进入一个路由时才把对应的组件加载进来,要实现这个很是简单,只须要一个webpack的loader:react-router-loader,先用npm把它安装上,而后修改src/routs.js文件,好比咱们如今想让登陆页面懒加载,那就把登陆页面的路由改为这样:

<Route path="login" component={require('react-router!./containers/Login')}/>

编译打包后,又多出了一个1.1.js文件,这就是在进入登陆路由时要加载的文件,也就是单独的登陆组件。其它的就不用咱们管了,代码会自动处理的。

既然是按需加载,咱们必定是但愿初始的时候加载的代码尽可能少,尽量在进入某个路由时才载入相应的所有内容。咱们的代码大体就三类东西:组件、action和reducer。组件很明显能够是独立载入的。reducer恐怕没办法,由于它须要指导整个仓库状态的创建。至于action,咱们前面的示例代码是不独立的,由于reducer要依赖action文件里面的常量,咱们只须要把全部的常量提出到一个公共的文件中,只有组件引用action文件。好比咱们新建一个src/consts.js文件,内容是:

export const INPUT_USERNAME = 'INPUT_USERNAME'
export const INPUT_PASSWORD = 'INPUT_PASSWORD'
export const RECEIVE_NEWS_LIST = 'RECEIVE_NEWS_LIST'
export const SET_KEYWORD = 'SET_KEYWORD'
// 全部action的常量...

而后还以login为例,把src/reducers/login.js里面引入常量的目标改成consts.js:

import {INPUT_USERNAME, INPUT_PASSWORD} from '../consts'

src/actions/login.js里也这样引入常量。run dist后,1.1.js文件就包含了actions/login.js里面的内容。

添加hash后缀

在一个大型且须要频繁升级的项目中,静态文件每每须要添加hash后缀,这主要是出于两个缘由:一个是全部版本的静态文件能够同时存在,而页面由后端控制,后端根据接口的版本绑定js和css文件,这样便于升级和回滚。另外一个是防止缓存,这和前面图片重命名为hash值是一个道理。

让webpack为文件名添加后缀很是简单,只须要在输出的文件名上加上[hash]就能够了。好比咱们想让app.js带上hash后缀,只须要在cfg/dist.js最后一句前面加上一句:

config.output.filename = 'app.[hash].js'

而对于插件生成的样式文件和公共js文件一样也是在文件名上加上[hash]就好了。

如今关键的问题是怎么应用这些有了hash后缀的文件。总不能每打一次包咱们就手动改一下index.html把。

webpack的配置文件是js,这就意味着这个配置文件是活的,咱们能够很容易把想作的事情经过代码实现。如今我要在每次打包后把index.html文件引入的js和css文件自动替换成带hash尾巴的形式,只需添加一个本身写的插件,其实就是一个函数。在cfg/dist.js里面的plugins数组里添加如下函数:

function() {
  this.plugin("done", function(stats) {
    let htmlPath = path.join(__dirname, '../dist/index.html')
    let htmlText = fs.readFileSync(htmlPath, {encoding:'utf-8'})
    let assets = stats.toJson().assetsByChunkName
    Object.keys(assets).forEach((key)=>{
      let fileNames = assets[key];
      ['js', 'css'].forEach(function(ext){
        htmlText = htmlText.replace(key+'.'+ext, fileNames.find(function(item){
          return new RegExp(key+'\\.\\w+\\.'+ext+'$').test(item)
        }))
      })
    })

    fs.writeFileSync( htmlPath, htmlText)
  });
}

很暴力,就是赤裸裸的node操做文件系统。这回dist文件夹中的index.html里引入的脚本和样式都是带hash的了。

在不少项目中,咱们前端要提供的可能不是一个引用好js和css的html文件,而是一个map文件,里面有静态文件的版本信息(hash值),这样后端就能直接把须要的静态文件挂上。能够本身写一个跟上面代码相似的插件输出一个map文件,也可在万能的npm找个插件,好比map-json-webpack-plugin。上面那个功能也能够试试replace-webpack-plugin。

 

到这里,这一系列关于react的博客就算告一段落了。其实我还想写一个关于测试的,由于react+redux的这种模式很是利于测试,不过我还在琢磨测试当中,等琢磨得差很少了也许会补上一篇。

🖐

 

上一节 【3】穿越spa的路由

相关文章
相关标签/搜索