# 正文

诶?这么快就到生产环境了吗?我们的开发环境还没彻底完善呢,之前遗留的手动刷新页面才能看到最新效果的问题呢?

是的,的确有些地方还需要优化,但这个过程就像是““盖房子”。

当我们配置生产环境后,这个“毛坯房”就算是建造完成了,他已经能“用”,而剩下的那些优化和处理相当于是后期的“装修”,是为了让我们住的更舒服,体验更好。

“毛坯房”就像是骨架,骨架是基础,有了骨架后面的优化才有立足之地。

所以,我们本节一起来进行生产环境的配置、构建以及项目部署,让我们的“毛坯房竣工”。

# 生产环境都需要做哪些处理呢?

这里主要是对webpack进行相关配置,相信大家都玩过,那咱们就开门见山了。

  • 对应的应该拥有一个独立的生产环境配置文件
  • 设置环境变量,区分开发和生产环境
  • 压缩js css资源,体积更小,提高下载速度
  • js分包,基础库和业务代码分别打包,可以提高缓存利用率,提高页面渲染效率,节省用户流量
  • 为打包的bundle名称配置hash值,这样有利于发布和资源缓存
  • 生成资源映射表,用于服务端使用
  • 有独立的发布命令
  • 可以在本机运行生产环境 server,方便本地调试
  • 开发环境做相应的调整

# 整体看资源的分布情况

先看下开发环境和生产环境的资源加载情况,然后下面再进行具体的实现。

image-20210214215247731

image-20210214215257779

通过上图可以比较清晰的看到生产环境和开发环境的差别。

下面来看具体实现

# 准备工作

安装所需 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'// 打包后的文件名
                }
            }
        }
    }

# 生成资源映射表,用于服务端使用

image-20210214215320571

//生成 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"
  },

到此全部配置完成,具体运行效果看下图

image-20210214215343090

# 项目部署

通常一个项目线上运行都会使用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 //开启监听,文件改动会自动重启

# 小结

本节我们完成了生产环境的搭建和配置,我们的“毛坯房”建成了, 完成了一个重大的里程碑。

客户端配置就像是我们开发单页时的配置一样, 差别主要在于如何处理服务端代码构建,以及静态资源的规划。

后面我们就需要对我们的“房子”进行装修了,也就是优化,有哪些需要优化地方呢?咱们后面接着聊。

本节代码地址

github.com/Bigerfe/koa… (opens new window)

阅读全文