[Nuxt系列03]发布上线之旅

千里之行,始于足下。古人老是言简意赅地阐述朴实的道理。不少关于梦想关于计划的事情,老是缺乏一个开始,而后又在开始以后缺乏一个坚持,最终夭折,无疾而终。好在咱们有了一个开始,并慢慢坚持了下来。javascript

随着开发进度的持续推动,终于,咱们也要面临项目部署上线的各类问题。怎样持续迭代,怎样控制代码质量,怎样发布,怎样保证应用在线上的稳定运行……又是茫茫多的问题搞得人欲仙欲死呀~css

1、从官方文档入手

首先来看一下 Nuxt文档的命令和部署章节前端

  • nuxt build 利用webpack编译应用,压缩 JS 和 CSS 资源(发布用)
  • nuxt start 以生产模式启动一个Web服务器 (nuxt build 会先被执行)

同时,在咱们初始化项目的 package.json 里有以下指令配置:java

{
  "build": "nuxt build",
  "start": "cross-env NODE_ENV=production node server/index.js",
}

复制代码

而后,咱们在终端执行 npm run build 指令,会获得以下目录结构的打包资源:node

├─ .nuxt/            
│    ├─ components/
│    ├─ dist/ 
│    ├─ views/
│    ├─ App.js 
│    ├─ client.js 
│    ├─ ...
│    └─ ... 

复制代码

最后执行 npm run start 指令,终端告诉咱们:Server listening on http://localhost:3000,浏览器中能够看到,与咱们在开发阶段执行 npm run dev 看到的画面将别无二致:webpack

start_img

这一阶段,程序将调用 nuxt.config.js | .nuxt/ | server/ | static/ 等文件(夹)下的代码来支撑整个应用的运行。ios

此时,整个应用愉快地运行在咱们地开发机上,一派欣欣向荣的景象呀!but……此时,咱们点了一个超连接,来到了一个新的页面,页面背后的程序在发生各类“化学反应”的同时遇到了一个 bug,因而整个服务崩掉了~~~nginx

试想一下,假如这是在生产环境发生的事件,大概是大型惊悚片现场足可媲美的画面吧。毕竟咱们没法保证可以彻底 catch 掉全部可能发生的错误,因此必需要解决掉单线程的 node 自己在必定程度上的不稳定性。固然 node 发展至今,生态以内早已有很多成熟的解决方案,要否则可能在多年之前早就能够打出 GG 离场了吧,也轮不到我等新人在这里“指手画脚”呀。因此咱们在项目中选择 pm2 来守护进程,以确保程序在线上的相对稳定。git

值得一提的是,我常常关注 Awesomes 站内一个叫 前端 TOP 100 的排行,里面网罗了时下流行的或颇具热度的前端技术。目前,Nuxt 排名第 94 位,而 pm2 排名第 56 位,而在一年前,二者都并未位列其中。先不谈这个排行的准确性权威性,但至少足以说明一些问题和趋势。ok,继续回到 pm2~github

2、PM2 简介

先扔一个 pm2 的传送门。能够看到首页上醒目地写着一行指令:npm install pm2 -g,仿佛在大声告诉咱们:“我能帮你管理你运行在线上的应用并让它活得好好的,快来全局安装我,帮你守护进程吧~”。

ok,照作。完毕后惯例 pm2 -v 查看安装结果。而后,终端来到咱们的项目目录下,确保已经执行了 npm run build 指令并成功获得了上文提到的打包资源,运行 pm2 start npm -- run start,就能够将咱们的 Nuxt 应用置于 pm2 的掌控之下了。

pm2start

如上图所示,pm2 帮咱们启动了一个名为 npm 的 node 进程,模式为 fork,接下来几项分别是版本号、pid、上次更新时间和重载次数、运行状态、cpu和内存使用状况、是否监控项目下的文件变动等信息,以上的大部分信息咱们均可以经过 cli 输入相应的指令进行控制。好比经过 pm2 start npm --name mynuxtdemo -- run start,就能够启动一个名为 mynuxtdemo 的应用(为啥不直接 pm2 start ./server/index.js 呢,遍历文档,彷佛没法经过 cli 设置环境变量,不会是俺年纪大了眼神儿很差吧...),如图:

pm2startwithname

下面来看一下几个经常使用的配置项:

# 给你的应用起个名儿吧
--name <app_name>

# 监控项目下的文件变动,一旦出现变动,重启
--watch

# 设置一个容许当前应用占用的内存上限,一旦超过了,重启
--max-memory-restart <200MB>

# 设置日志的输出路径
--log <log_path>

# 为脚本传递额外的参数
-- arg1 arg2 arg3

# cluster模式下开启示例的数量,当设置为 max 时,根据当前主机的 cpu 核数设置
-i <instance_num>

复制代码

最终,咱们能够经过 cli 指令 pm2 start npm --watch -i max --name mynuxtdemo -- run start 来启动应用。以个人电脑为例,将启动一个名为 mynuxtdemo 的 Nuxt 应用,pm2 调用了 node 的 cluster 模块,根据 cpu 核数开启了 8 个实例,同时 pm2 将监听当前项目下的文件,当文件出现变动时,pm2 会尝试重启全部的 8 个实例以实现更新。

pm2cluster

而后咱们来看一下在程序意外终止的状况下会发生什么:

pm2reload

此时,我在 server/index.js 下抛出了一个自定义异常 throw 'this is a test error',能够看到 pm2 在程序终止时在不断尝试重启的过程。

ok,至此关于 pm2 的简单介绍即将告一段落,可是每次须要启动程序的时候都须要这样一长串指令的输入也是一件很痛苦的事情,至少对于我这种"老年人记性"是不太可以接受的~

还好 pm2 提供了配置文件的方式来描述咱们须要它做出何种行为,以知足咱们的预期,就像 gulp | eslint ... 它们那样,一样提供了多种多样的文件格式,总有一款符合你的口味。详情见 PM2 Ecosystem File 章节。这里,咱们新建了一个 pm2.config.json 文件来描述咱们对 pm2 提出的“要求”:

{
  "apps": [
    {
      "name": "mynuxtdemo",
      "script": "./server/index.js",
      "instances": 4, 
      "max_memory_restart": "500M",
      "watch": [
        "./server",
        "./nuxtdist"
      ],
      "ignore_watch": [
        "./nuxtdist/dist/client"
      ],
      "env": {
        "NODE_ENV": "production",
        "DEMO_ENV": "prod"
      },
      "env_tset": {
        "NODE_ENV": "production",
        "DEMO_ENV": "test"
      },
      "env_dev": {
        "NODE_ENV": "development",
        "DEMO_ENV": "dev"
      },
      "output": "./logs/out.log",
      "error": "./logs/error.log",
      "log_date_format":"YYYY-MM-DD HH:mm CCT",
    }
  ]
}

复制代码

而后,咱们在 package.json 中做以下配置:

{
  "scripts": {
    "dev": "cross-env DEMO_ENV=dev NODE_ENV=development nodemon server/index.js --watch server",
    "build": "cross-env DEMO_ENV=prod NODE_ENV=production nuxt build",
    "start": "cross-env DEMO_ENV=prod NODE_ENV=production node server/index.js",
    "pm2start": "pm2 start pm2.config.json",
    "buildtest": "cross-env DEMO_ENV=test NODE_ENV=production nuxt build",
    "starttest": "cross-env DEMO_ENV=test NODE_ENV=production node server/index.js",
    "pm2test": "pm2 start pm2.config.json --env test"
  }
}

复制代码

从此,咱们只要执行 npm run pm2start 就能够开始愉快地玩耍了~

pm2withconfigfile

固然了,上文所及仅是 pm2 的冰山一角,它还有不少的配置选项能够知足咱们这样那样的需求,还请通读文档,并待往后带着问题找答案吧。 下面列几个平时经常使用的 pm2 指令:

pm2 list
pm2 logs
pm2 show 0
pm2 reload all
pm2 start 0
pm2 delete all
pm2 flush
#...

复制代码

3、将打包产物上传至线上

终于,在某一天,当前的开发进度渐渐接近尾声了,咱们也要尝试让应用运行在生产环境下来一睹其风采了。毕竟对于咱们整个团队来讲,Nuxt 仍是一个陌生的框架,早作准备总没有错。

首先,咱们使用 nginx 来做反向代理,Nuxt 文档很贴心地给处理一个示例代码(好久之前彷佛是木有的?😢),戳 Nginx 使用nginx做为反向代理 围观。为 nginx 添加本项目的配置文件,好比在 nginx/conf.d/ 下新建一个 mynuxtdemo.conf 文件,并有以下配置:

upstream nuxt_demo {
   server 127.0.0.1:3000;
}

server {
  listen      80;
  server_name test.mynuxtdemo.com;

  gzip            on;
  gzip_types      text/plain application/xml text/css application/javascript;
  gzip_min_length 1000;

  location / {
      proxy_http_version 1.1;

      proxy_redirect                      off;

      proxy_set_header Host $host;
      proxy_set_header X-Real-IP          $remote_addr;
      proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto  $scheme;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_set_header X-Nginx-Proxy true;
      proxy_cache_bypass $http_upgrade;

      proxy_pass http://nuxt_demo;
      # ...
  }
}

复制代码

重启 nginx 使配置生效,而后将打包后的资源(nuxt.config.js | .nuxt/ | server/ | static/...etc)上传至服务器,项目根目录下npm i --production,依赖安装后,继续执行 npm run pm2start,而后在浏览器访问 test.mynuxtdemo.com 就能够访问咱们的 nuxt 应用了。

此时,个人手里有了一个生产环境下直达该项目根目录的 ftp 帐号,用于项目迭代过程当中上传新版本的打包产物。项目小分队们继续作正式上线前的开发工做,某一天在好几个时间段完成了好几个新的功能并伴随着产品同窗永不停歇的改这改那的碎碎念,因而 npm run build >> ftp upload >> npm run build >> ftp upload ...。更不能忍受的是,还要不停地把 .nuxt/dist/client/ 下的资源上传到阿里云 oss 上。 当时个人心里是绝望的,来回折腾,效率低下,还容易出错,哭。

总之,遇到问题就要解决问题,此时脑海中闪过那句话:你遇到的 99% 的问题均可以在互联网上找到答案。因而果断投入 npm 和 github 的怀抱寻求解救,并顺利找到答案。通过一番筛选,最终选择了两个插件来做为辅助上传的工具,分别是:ftp 客户端 node 版本阿里云 oss 上传插件。基于这两个插件,并根据咱们现阶段的需求,分别编写了 ftp 和 oss 的上传脚本:

// uploader/ftp.js
const fs = require('fs');
const path = require('path');
const ftp = require('ftp');
const log = require('single-line-log').stdout;
// ftp服务器参数配置
const ftp_config_hjxy = {
  host: '1xx.xxx.xxx.xxx',
  port: 21,
  user: "username",
  password: "password"
}

const topPath = '/nuxtdist'; // 服务器目标路径(咱们的dist目录配置为 nuxtdist/)
const dirpath = path.resolve(__dirname, '../nuxtdist'); // 本地须要上传的文件夹路径

let streamList = [];
let mkDirList = [];
let streamAmount = 0;
streamFactory(dirpath, topPath);

const c = new ftp();
c.connect(ftp_config_hjxy); //连接ftp服务器
c.on('ready', function (err) {  //准备操做
  if(err) throw err;
  console.log('\x1B[32m√\x1B[39m connection successful.');
  // 先建立可能不存在的文件夹
  mkDirList.forEach((dirPath) => {
    c.mkdir(dirPath, false, (err) => {
      err ? console.log(`\x1B[33m建立文件夹${dirPath}失败--已存在/未知错误-${err}\x1B[0m`) : console.log(`\x1B[32m建立文件夹${dirPath}成功\x1b[39m`);
    });
  });
  // 而后开始上传文件
  streamList.forEach((fileItem, idx) => {
    c.put(fileItem.stream, fileItem.destPath, false, (err) => {
      if (err) { console.log(`上传${fileItem.destPath}失败${err}`); }
      log(`\x1B[32m文件已上传-${idx + 1}/${streamAmount}-\x1b[39m`);
      (idx + 1 === streamAmount) && c.end();
    });
  });
});
c.on('error', function (err) {
  if(err) throw err;
  console.log("err", err);
});

/** * 生成stream流列表 * @param {String} dirpath * @param {String} destPath */
function streamFactory(dirpath, destPath) {
  const files = fs.readdirSync(dirpath);
  files.length && files.forEach((filename) => {
    const filepath = path.resolve(dirpath, filename);
    const isDir = fs.statSync(filepath).isDirectory();
    const isFile = fs.statSync(filepath).isFile();
    if(isFile) {
      const stream = fs.createReadStream(filepath);
      streamList.push({ stream, destPath: `${destPath}/${filename}` });
      streamAmount++;
    }else if (isDir) {
      if(filename === 'client') return;
      mkDirList.push(`${destPath}/${filename}`)
      streamFactory(filepath, `${destPath}/${filename}`);
    }
  });
}

复制代码

这里有一个小 bug,服务器中 topPath 的目录必须存在。因为除了打包产出的资源之外的文件都相对稳定,而且 server/ 下的代码更新须要格外谨慎,因此这里只作打包资源的上传,并剔除了 .nuxt/dist/client/ 下的文件。接下来就须要把 .nuxt/dist/client/ 下的文件上传至 oss 了,思路上与上面 ftp 上传的代码大同小异。

// uploader/oss.js
const OSS = require('ali-oss');
const fs = require('fs');
const path = require('path');
const log = require('single-line-log').stdout;

const client = new OSS({
  region: 'your oss region',
  accessKeyId: 'your oss accessKeyId',
  accessKeySecret: 'your oss accessKeySecret',
  bucket: 'nuxtdemo',
});

const aliyunOss = {
  bucket: 'nuxtdemo',
  site: 'your site addr',
  dirName: '/nuxtclient'
};

const dirpath = path.resolve(__dirname, '../nuxtdist/dist/client');
let streamList = [];
let streamAmount = 0;
streamFactory(dirpath, aliyunOss.dirName);
streamList.forEach((fileItem, idx) => {
  putStream(fileItem.destPath, fileItem.stream);
  log(`\x1B[32m--正在上传${idx + 1}/${streamAmount}--\x1B[39m`);
  (idx + 1 === streamAmount) && log('\x1B[32m上传完成\x1B[39m');
});

/** * aliyunoss 流式上传 * @param {String} filename 上传至oss使用的文件名 * @param {String} stream 可读的文件流 */
async function putStream (filename, stream) {
  try {
    await client.putStream(filename, stream);
  } catch (err) {
    throw err;
  }
}

/** * 生成stream流列表 * @param {String} dirpath 本地须要上传的目录位置 * @param {String} destPath 上传至服务器的目录位置 */
function streamFactory(dirpath, destPath) {
  const files = fs.readdirSync(dirpath);
  files.length && files.forEach((filename) => {
    const filepath = path.resolve(dirpath, filename);
    const isDir = fs.statSync(filepath).isDirectory();
    const isFile = fs.statSync(filepath).isFile();
    if(isFile) {
      const stream = fs.createReadStream(filepath);
      streamList.push({ stream, destPath: `${destPath}/${filename}` });
      streamAmount++;
    }else if (isDir) {
      streamFactory(filepath, `${destPath}/${filename}`);
    }
  });
}

复制代码

而后,咱们在 package.json 中增添以下两条新的配置:

{
  "script": {
    "oss": "node uploader/oss.js",
    "ftp": "node uploader/ftp.js"
  }
}

复制代码

这样,只须要在打包完毕后,分别执行 npm run ossnpm run ftp 就能够将新的代码安排到服务器了。

考虑到常常要进行 oss 上传,因而又将 oss.js 部分封装成一个 npm oss上传插件,这样之后只须要在须要用到的地方 npm install --save-dev @crazymuyang/alioss-uploader 安装,并增长一个简单的配置文件就可使用了:

const aliOssUploader = require('@crazymuyang/alioss-uploader');
const path = require('path');
const aliossConfig = {
  region: 'your oss region',
  accessKeyId: 'your oss accessKeyId',
  accessKeySecret: 'your oss accessKeySecret',
  bucket: 'your oss bucket',
};
const uploadConfig = {
  dirpath: path.resolve(__dirname, './test'),  // 将该路径下的文件上传至oss()
  destpath: '/test',  // 将文件上传至bucket下的该路径下
}

const uploader = new aliOssUploader(aliossConfig, uploadConfig);
uploader.start();

复制代码

4、你觉得结束的时候,其实“灾难”刚刚开始

就这样,跌跌撞撞中项目能够勉强上线了。然而,咱们发如今 ftp 上传的过程当中会进入长时间的 502 状态,此时 pm2 的 watch 功能不断监测到文件变动,同时不断地尝试重启实例,直到最后一个文件上传完毕,整个应用恢复正常。此时,咱们执行 pm2 ls 查看会发现 reload 的次数增长了好屡次。

这样确定是不行的呀,因而便采用了一个折中的办法,为 nginx 配置 502 跳转页面(loading),而后在这个页面里经过定时器在一段时间后再跳回到项目域名下。此时,在文件上传过程当中就是一个路由来回重定向的过程,用户视角下就是有一段时间应用一直停留在一个 loading 页面。这样有一个很明显的弊端,从 loading 页面回来只能去往首页,而没法跳回以前用户访问的页面。

因此,问题来了:到底怎样才能实现真正地不停机更新呢?其实答案很简单,之因此会长时间处于 502 状态,仅仅是由于短期内 pm2 监听了大量文件变动,它跟不上趟了。

如何解决,以实现真正的不停机呢?且听下回分解~

相关文章
相关标签/搜索