# 正文
诶?这么快就到生产环境了吗?我们的开发环境还没彻底完善呢,之前遗留的手动刷新页面才能看到最新效果的问题呢?
是的,的确有些地方还需要优化,但这个过程就像是““盖房子”。
当我们配置生产环境后,这个“毛坯房”就算是建造完成了,他已经能“用”,而剩下的那些优化和处理相当于是后期的“装修”,是为了让我们住的更舒服,体验更好。
“毛坯房”就像是骨架,骨架是基础,有了骨架后面的优化才有立足之地。
所以,我们本节一起来进行生产环境的配置、构建以及项目部署,让我们的“毛坯房竣工”。
# 生产环境都需要做哪些处理呢?
这里主要是对webpack进行相关配置,相信大家都玩过,那咱们就开门见山了。
- 对应的应该拥有一个独立的生产环境配置文件
- 设置环境变量,区分开发和生产环境
- 压缩
js css资源,体积更小,提高下载速度 js分包,基础库和业务代码分别打包,可以提高缓存利用率,提高页面渲染效率,节省用户流量- 为打包的
bundle名称配置hash值,这样有利于发布和资源缓存 - 生成资源映射表,用于服务端使用
- 有独立的发布命令
- 可以在本机运行生产环境
server,方便本地调试 - 开发环境做相应的调整
# 整体看资源的分布情况
先看下开发环境和生产环境的资源加载情况,然后下面再进行具体的实现。


通过上图可以比较清晰的看到生产环境和开发环境的差别。
下面来看具体实现
# 准备工作
安装所需 npm 包
npm i optimize-css-assets-webpack-plugin 压缩 css
npm i uglifyjs-webpack-plugin 压缩 js
npm i mini-css-extract-plugin 提取 css ,上一节已介绍过
npm i clean-webpack-plugin 打包前清理 dist 目录
# 前端生产环境构建配置
css js相关的 loader 配置和我们前面介绍的开发环境的一致,所以这里仅介绍有区别的地方。
首先我们创建生产环境配置文件./webpack/webpack.prod.config.js
# 配置环境变量
plugins: [
//...
new webpack.DefinePlugin({
'process.env': { NODE_ENV: '"production"'},//标识生产环境
'__IS_PROD__': true//方便在代码中使用
})
//...
]
# 配置文件 hash
方便文件的发布和充分利用资源的强缓存
//...
output: {
//设置 js
filename: 'js/[name].[chunkhash:8].js',
path: resolvePath('../dist/static'),
publicPath: '/'
}
//...
new MiniCssExtractPlugin({
//设置 css
filename:'css/[name].[contenthash:8].css'
})
//...
# 压缩 js css 文件
使其体积更小,提高下载速度
//压缩和优化 css
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
//压缩 js 代码
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
//...
optimization: {
minimizer: [
//压缩 js
new UglifyJsPlugin({
uglifyOptions: {
compress: {
drop_console: true,
drop_debugger: true
},
warnings: false,
ie8: true,
output: {
comments: false,
},
},
cache: true,
sourceMap: false
}),
//压缩 css
new OptimizeCSSAssetsPlugin()
]
}
# js 分包
将基础库和业务代码分别打包
optimization: {
//...
splitChunks: {
cacheGroups: {
libs: { // 抽离第三方库
test: /node_modules/, // 指定node_modules下的包
chunks: 'initial',
name: 'libs'// 打包后的文件名
}
}
}
}
# 生成资源映射表,用于服务端使用

//生成 manifest 方便定位对应的资源文件
const ManifestPlugin = require('webpack-manifest-plugin');
//...
plugins: [
//生成 manifest
new ManifestPlugin({
fileName: '../server/asset-manifest.json',
})
],
# 配置构建命令
npm run client:build
// ./package.json
"client:build": "NODE_ENV=production webpack --config ./webpack/webpack.prod.config.js",
这仅仅是前端资源的构建,还缺少服务端代码的构建。
# 完整前端生产环境配置代码
// webpack/webpack.prod.config.js
//生产环境配置
//webpack 配置文件
const path = require('path')
const webpack = require('webpack');
//提取 css 插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
//生成 manifest 方便定位对应的资源文件
const ManifestPlugin = require('webpack-manifest-plugin');
//压缩 js 代码
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
//构建前清理目录
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin');
//压缩和优化 css
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
//路径转换
const resolvePath = (pathstr) => path.resolve(__dirname, pathstr);
process.env.BABEL_ENV ='development';//指定 babel 编译环境
module.exports = {
mode: 'production',
devtool: 'none',
entry: {
main: [resolvePath('../src/client/app/index.js')] //指定一个入口名称
},
output: {
filename: 'js/[name].[chunkhash:8].js',
path: resolvePath('../dist/static'),
publicPath: '/'
},
module: {
rules: [{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(sa|sc|c)ss$/,
use: [{
loader: MiniCssExtractPlugin.loader,
},
{
loader: "css-loader",
},
{
loader: "postcss-loader"
},
{
loader: "sass-loader"
},
]
},
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]',
publicPath: '/'
}
}]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css'
}),
// 清理上一次构建的文件
new CleanWebpackPlugin(),
//生成 manifest 方便定位对应的资源文件
new ManifestPlugin({
fileName: '../server/asset-manifest.json',
}),
new webpack.DefinePlugin({
'process.env': { NODE_ENV: '"production"'},
'__IS_PROD__': true
})
],
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
compress: {
drop_console: true,
drop_debugger: true
},
warnings: false,
ie8: true,
output: {
comments: false,
},
},
cache: true,
parallel: true,
sourceMap: false
}),
new OptimizeCSSAssetsPlugin()
],
splitChunks: {
cacheGroups: {
libs: { // 抽离第三方库
test: /node_modules/, // 指定是node_modules下的第三方包
chunks: 'initial',
name: 'libs'// 打包后的文件名,任意命名
}
}
}
}
}
# 服务端生产环境配置
我们需要在已有的配置webpack.server.config中兼容生产环境的配置。
# 定义环境变量
在服务端就可以通过以下变量来判断当前环境。
process.env.NODE_ENV:production|development
__IS_PROD__:true|false 简易访问
//获取当前环境
const isProd=process.env.NODE_ENV==='production';
//...
plugins:[
new webpack.DefinePlugin({
'process.env': { NODE_ENV: `"${process.env.NODE_ENV}"`},
'__IS_PROD__':isProd
})
]
//...
# 定义生产目录别名
因为在服务端执行需要导入静态资源映射表asset-manifest.json,定义别名更方便导入。
//...
resolve: {
alias: {
//定义 dist 目录别名,方便导入模块
'@dist': path.resolve(__dirname,'../dist')
}
}
/...
# 服务端代码调整
服务端需要根据当前环境来启动,生产环境需要得到js css 资源文件,然后作为资源和 html 结构一同直出给浏览器端。
# 创建资源处理模块
新建src/server/common/assets.js模块,用于服务端对静态资源的读取。
下面代码是一个完整的模块,最终返回静态资源的 html 标记。
//生产环境中 静态资源的处理
module.exports = function () {
let devHost = '//localhost:9001';
let jsFiles = ['libs.js','main.js'];
let cssFiles = ['main.css'];
const assets = {
js: [],
css: []
};
if (!__IS_PROD__) {//开发环境
assets.js.push(`<script type="text/javascript" src="${devHost}/libs.js"></script>`);
assets.js.push(`<script type="text/javascript" src="${devHost}/main.js"></script>`);
assets.css.push(`<link rel="stylesheet" type="text/css" href="${devHost}/main.css" />`);
} else {
//生产环境 从 asset-manifest.json 读取资源
const map = require('@dist/server/asset-manifest.json');
jsFiles.forEach(item => {
if(map[item])
assets.js.push(`<script type="text/javascript" src="${map[item]}"></script>`)
});
cssFiles.forEach(item => {
if(map[item])
assets.css.push(`<link rel="stylesheet" type="text/css" href="${map[item]}" />`)
});
}
return assets;
}
# ssr 中间件 react-ssr.js 调整
//导入资源处理库
const getAssets = require('../common/assets');
//得到静态资源
const assetsMap = getAssets();
//绑定资源
//...
ctx.body=`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${tdk.title}</title>
<meta name="keywords" content="${tdk.keywords}" />
<meta name="description" content="${tdk.description}" />
${assetsMap.css.join('')} //输出 css 资源
</head>
<body>
<div id="root">
${html}
</div>
<textarea id="ssrTextInitData" style="display:none;">
${JSON.stringify(fetchResult)}
</textarea>
</body>
</html>
</body>
${assetsMap.js.join('')} //输出 js 资源
`;
//...
# 服务端生产环境构建命令
配置 npm scripts 命令,同时设置环境变量为NODE_ENV=production,必须要有此设置,否则会按照开发环境执行。
"server:build": "NODE_ENV=production webpack --config ./webpack/webpack.server.config.js"
# 生产环境服务启动
方便本地查看和调试
npm run prod:start
"prod:start": "node ./dist/server/app.js"
# 开发环境调整
因为最初在开发环境对js资源打包没有分包,所以这里要和生产环境统一。
这里为了方便演示,生产环境和开发环境采用的两个独立的配置文件,其实可以将相同的配置提取出来,然后使用webpack-merge进行合并。
// ./webpack/src/webpack-dev.config.js
//...
optimization: {
splitChunks: {
cacheGroups: {
libs: { // 抽离第三方库
test: /node_modules/, // 指定是node_modules下的第三方包
chunks: 'initial',
name: 'libs'// 打包后的文件名,任意命名
}
}
}
}
//...
# 最终的构建命令
npm run build
"scripts": {
"build": "NODE_ENV=production npm run client:build && npm run server:build",
"client:build": "NODE_ENV=production webpack --config ./webpack/webpack.prod.config.js",
"server:build": "NODE_ENV=production webpack --config ./webpack/webpack.server.config.js",
"prod:start": "node ./dist/server/app.js"
},
到此全部配置完成,具体运行效果看下图

# 项目部署
通常一个项目线上运行都会使用nginx做反向代理,请求都会发到nginx服务,然后再转发到我们的node服务,而node服务的进程守护一般都会用pm2来做。
下面代码便是我们使用pm2来启动服务
PORT=9001 pm2 start ./dist/server/app.js -n xxx.com -o "/data1/logs/xxx.com.-out.log" -e "/data1/logs/xxx.com-err.log" --watch
//参数说明
PORT=9001 //指定服务启动的端口
pm2 start ./dist/server/app.js //服务入口文件
-n xxx.com //设置服务名称
-o xx //设置日志文件
-e xx //设置错误日志文件
--watch //开启监听,文件改动会自动重启
# 小结
本节我们完成了生产环境的搭建和配置,我们的“毛坯房”建成了, 完成了一个重大的里程碑。
客户端配置就像是我们开发单页时的配置一样, 差别主要在于如何处理服务端代码构建,以及静态资源的规划。
后面我们就需要对我们的“房子”进行装修了,也就是优化,有哪些需要优化地方呢?咱们后面接着聊。
本节代码地址