部署React+Redux Web App∞
March 09, 2016
前段时间使用React+Redux做了个后台管理的项目,在React初体验中分享了下入门经验。这篇文章谈谈我的部署实践。
目标
怎样才是好的部署呢?我觉至少有以下2点:
- 性能优化:包括代码执行速度、页面载入时间
- 自动化:重复的事情尽量让机器完成,最好能运行一条命令就完成部署
代码层面
首先从代码层面来分析。
使用React+Redux,往往会用到其强大的调试工具Redux DevTools。在手动配置DevTools时需要围绕Store、Component进行一些配置,然而这些都是用来方便调试的,生产环境下我们不希望加入这些东西,所以建议就是从代码上隔离development和production环境:
containers/
Root.js
Root.dev.js
Root.prod.js
...
store/
index.js
store.dev.js
store.prod.js
同时采用单独的入口文件(比如上面的containers/Root.js
)按需加载不同环境的代码:
if (process.env.NODE_ENV === 'production') { module.exports = require('./Root.prod'); } else { module.exports = require('./Root.dev'); }
有一个细节需要注意:ES6语法不支持在if中写import语句,所以这里采用了CommonJS的模块引入方法require
。
具体可以看看Redux的Real World示例项目。
代码层面还需要注意的一点就是按需import,否则可能会在打包时生成不必要的代码。
OK,我们现在用webpack打个包,webpack --config webpack.config.prod.js --progress
,结果可能会让你下一跳:8.4 M!求心理阴影面积…
使用webpack打包
接下来我们来调教下打包工具。目前React主流打包工具有2种:webpack、Browserify。Browserify没用过,这里主要谈谈webpack的配置经验。
同上,建议为不同的环境准备不同的webpack配置文件,比如:webpack.config.dev.js
、webpack.config.prod.js
。下面我们来看看几个比较关键的配置选项:
devtools
文档在这里,我对source map技术不太了解,所以几个选项真不知道是干什么的。不过好在下面的表格中有写哪些是production supported,随便选择一个就好,感觉结果区别不大。这里我选择了source-map
,webpack一下后生成了2个包:
- bundle.js:3.32 MB
- bundle.js.map:3.78 MB
唔,这样好多了,把用于定位源码的source map分离出去了,一下子减少了一半以上的体积。(注:source map只会在浏览器devtools激活时加载,并不会影响正常的页面加载速度,具体可参考When is jQuery source map loaded?、JavaScript Source Map 详解。)
plugins
webpack文档中有一节Optimization,讲到了一些优化技巧。Chunks略高级没用过,看前面两个吧。提到了3个插件:UglifyJsPlugin、OccurenceOrderPlugin、DedupePlugin,第一个插件应该都懂是干啥,后面两个描述得挺高深的,不过不懂没关系,全用上试试,反正没副作用:
plugins: [ new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }), new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurenceOrderPlugin() ]
打包结果:1.04 MB。
不要忽视NODE_ENV
NODE_ENV其实就是一个环境变量,在Node中可以通过process.env.NODE_ENV
获取。目前大家往往用这个环境变量来标识当前到底是development还是production环境。
React提供了2个版本的代码(见:Development vs. Production Builds),production版性能更好:
We provide two versions of React: an uncompressed version for development and a minified version for production. The development version includes extra warnings about common mistakes, whereas the production version includes extra performance optimizations and strips all error messages.
同时在React文档中明确建议在生产环境下设置NODE_ENV
为production
(见:npm):
Note: by default, React will be in development mode. To use React in production mode, set the environment variable NODE_ENV to production (using envify or webpack’s DefinePlugin). A minifier that performs dead-code elimination such as UglifyJS is recommended to completely remove the extra code present in development mode.
可以通过webpack的DefinePlugin设置环境变量,如下:
plugins: [ ... new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }), ]
打包结果:844 KB。
虽然比之前的1 M减少得不多,不过可以提升React的运行性能,还是很值的。
OK,webpack到此为止,给出完整的webpack.config.prod.js
:
var path = require('path'); var webpack = require('webpack'); module.exports = { devtool: 'source-map', entry: [ './index.js' ], output: { path: path.join(__dirname, 'webpack-output'), filename: 'bundle.js', publicPath: '/webpack-output/' }, plugins: [ new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }), new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }), ], module: { loaders: [ { test: /.js$/, loader: 'babel', exclude: /node_modules/, include: __dirname }, { test: /\.css$/, loaders: ["style", "css"] }, { test: /\.scss$/, loaders: ["style", "css", "sass"] } ] }, };
打包结果输出到webpack-output
文件夹下。
使用FIS3添加hash
前端公认的Best Practice就是给资源打上hash标签,这对缓存静态资源很有用。webpack文档中有一节Long-term Caching就是专门讲这个的,然而配置起来好麻烦的样子,最后我还是选择了百度的FIS3。
使用方法见文档,写得很详细。贴一下我的fis-conf.js
:
// 需要打包的文件 fis.set('project.files', ['index.html', 'static/**', 'webpack-output/**']); // 压缩CSS fis.match('*.css', { optimizer: fis.plugin('clean-css') }); // 压缩PNG图片 fis.match('*.png', { optimizer: fis.plugin('png-compressor') }); fis.match('*.{js,css,png}', { useHash: true, // 启用hash domain: 'http://7xrdyx.com1.z0.glb.clouddn.com', // 添加CDN前缀 });
其中,通过useHash: true
启用了hash功能,同时压缩了CSS、PNG图片,然后通过domain
添加了CDN前缀。
运行fis3 release -d ./output
后,就把所有的文件打包到output
文件夹下了,截个图:
使用CDN
844 KB虽然比最开始的8.4 M缩小到了1/10,但其实也有点大。包大小基本上已经压缩到极限了,但我们还可以通过CDN来加快页面加载时间。
我选择的是七牛,效果不错,而且免费额度够用。
上一步中我们已经用FIS3添加了七牛CDN的前缀,接下来就是上传打包文件了。手动上传太麻烦,七牛提供了一个用来批上传的命令行工具qrsync,具体用法见文档。
使用Fabric进行远程部署
部署的时候难免会涉及到登陆server执行部署命令,你可以手动操作,但我还是推荐用一些工具来做,方便自动化。这类工具不少,选择顺手的就行,我因为之前有过Python开发经验,所以一直用Fabric,很好用。安装下Python,然后安装包管理工具pip,然后sudo pip install fabric
就行了。
在项目根目录下创建fabfile.py
,通过Python代码描述远程部署过程:
# coding: utf-8 from fabric.api import run, env, cd def deploy(): env.host_string = "username@ip" with cd('/path/to/your/project'): run('git pull') run('npm install') run('webpack --progress --config webpack.config.prod.js') run('fis3 release -d ./output') run('qrsync qrsync.conf.json')
其中,env.host_string
描述server信息,然后cd到项目文件夹,git pull
从Git仓库拉取源码,npm install
安装第三方库,接下来就是各种打包,最后批量上传到CDN。
本地执行fab deploy
,就可以部署到生产服务器了。
Nginx
收尾工作交给Nginx:
- 域名与本地文件夹路径关联起来
- gzip支持:这个一定要做,效果很赞,具体启用方法就是将
/etc/nginx/nginx.conf
与gzip相关的东西uncomment一下就行 - 不存在的path一律导向
/index.html
:否则在非根路径下刷新浏览器,就会出现404,开发React的童鞋应该都懂这个坑…
我的nginx.conf
如下所示:
server {
listen 80;
server_name yourdomain.com;
root /path/to/your/project;
location / {
try_files $uri /index.html;
}
}
注:有童鞋可能奇怪为什么没有添加cache的配置,因为所有东西都上传到CDN了…
浏览器实际加载效果
在Chrome调试工具下看。
禁止缓存:
可以看到bundle的最终大小为206 KB,加载时间是118 ms。
启用缓存:
效果还不错。
开发->部署流程
从开发到部署的流程如下:
- 写代码、本地调试
- 代码提交到远程Git仓库
- 部署:
fab deploy
附:使用npm scripts
最近npm scripts有点火,很多人都用它来取代Grunt、Gulp做自动化构建。
我们将部署命令放到package.json
的scripts
中,然后通过npm run <script-name>
的方式调用不同的script,这样会更加的cleaner:
{ "name": "your-project-name", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "node server.js", "build": "webpack --progress --config webpack.config.prod.js && fis3 release -d ./output", "upload": "qrsync qrsync.conf.json", "deploy": "fab deploy" }, ... }
然后fabfile.py
可以改写为:
# coding: utf-8 from fabric.api import run, env, cd def deploy(): env.host_string = "user@ip" with cd('/path/to/your/project'): run('git pull') run('npm install') run('npm run build') run('npm run upload')
部署命令变成:npm run deploy
,更加赏心悦目。