【前端打包部署】谈一谈我在SPA项目打包=>部署的处理

5,215 阅读7分钟

前言

在上篇《【vue-cli3升级】老项目提速50%(二)》文中,评论区好几个人对文中task任务以及shell打包推送远程仓库表示感兴趣,希望我多描述些内容... 基于此原因、在此基础上,谈一谈我在SPA项目打包 => 部署的工程化内容

PS:只作为本人工作的分享,不作为任何形式的指南、教程、最佳实践等,也并非手把手系列

项目环境描述

  • SPA项目(Vue-cli3)
  • Gitlab代码仓库
  • node 10.8.0
  • npm 6.5.0
  • Nginx服务器
  • Linux系统
  • Jenkins持续集成部署工具

说下上篇文中Task.jsrun.sh

以下内容请结合《【vue-cli3升级】老项目提速50%(一)》《【vue-cli3升级】老项目提速50%(二)》阅读

为什么这么做?

结合实际场景罗列两个吧...

  • 为了随时发布前端项目,不用等到晚上加班发布...
  • 曾经,在发布过程中,老板正在浏览…然后白屏了…所以为了发布时线上用户无感知

整体流程(画图真墨迹啊)

  1. 执行 run.sh ,以 beta 环境为例,项目根目录执行 ./run.sh beta

  2. run.sh 中,初始化beta环境的远程仓库,拉取代码到本地 .deploy/beta 目录,将 .deploy/beta 目录中的文件复制到本地 dist 目录

  3. run.sh 中,执行构建: npm run build_beta

  4. vue.config.js 中,创建版本号 lastVersion,将版本号 lastVersion 加入到 cssjs 资源文件名中(我是加在最前面的,位置随意,和 task.js 中的正则对应)

  5. vue.config.js 中,注入 task.js ,执行任务

    const task = require('./task')
    task.run(lastVersion)
    
  6. task.js 中,将版本号写入 history.js ,判断是否超出最大值(最大5个历史版本),超出则执行 rmFiles() ,删除本地 dist 目录中最老的版本资源

  7. 复制本地 dist 目录文件到 .deploy/beta 中,提交 .deploy/beta 文件到远程仓库

附图:了解下.deploy/beta目录文件,history.js文件,远程Gitlab仓库代码

【Task.js】

前提:打包时不清除dist文件目录,以增量的形式打包。

vue-cli3下可参考《【vue-cli3升级】老项目提速50%(二)》

vue-cli2下在 /build/build.js 中找到以下代码注释掉

// rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
//   if (err) throw err
// })

何时执行?build构建时

作用:利用nodeJs的fs模块操作history.js,存储最多5个版本,超过5个在本地dist目录中删除匹配最老版本的相关资源

代码(基本上都加上注释描述了)

const fs = require('fs') // node fs模块,操作文件
const path = require('path') // node path模块,操作路径
const endOfLine = require('os').EOL // node os模块,不同操作系统的行尾符

module.exports = {
  maxHistoryNum: 5, // 控制存储版本最大值
  historyFile: path.resolve(__dirname, './dist/history.js'), // 本地dist目录中的history.js
  staticDir: path.resolve(__dirname, './dist/'), // 本地dist目录
	// history.js不存在,创建
  creataHistoryIfNotExist () {
    if (!fs.existsSync(this.historyFile)) {
      this.storeHistory([], 'a+')
    }
  },

  // @done 将数据写到 history.js
  storeHistory (list, mode) {
    let historyFile = this.historyFile
    let outJson = 'module.exports = [' + endOfLine
    let listLen = list.length
    if (list && listLen > 0) {
      list.forEach((item, index) => {
        if (index === listLen - 1) {
          outJson += `  ${item}${endOfLine}`
        } else {
          outJson += `  ${item},${endOfLine}`
        }
      })
    }
    outJson += ']' + endOfLine
		// 以 a+ 的falg标志在history.js写入和追加内容
    fs.writeFileSync(historyFile, outJson, {
      flag: mode
    })
  },

  // 递归删除目录中的文件 (构建完成时版本>最大值时执行)
  rmFiles (dirPath, regexp) {
    let files
    try {
      files = fs.readdirSync(dirPath)
    } catch (e) {
      return
    }
    if (regexp && files && files.length > 0) {
      for (let i = 0; i < files.length; i++) {
        let filename = files[i]
        let filePath = dirPath + '/' + files[i]
        if (fs.statSync(filePath).isFile() && regexp.test(filename)) {
          console.log('删除过期的历史版本->(' + regexp + '):' + filename)
          fs.unlinkSync(filePath)
        } else {
          this.rmFiles(filePath, regexp)
        }
      }
    }
  },

  // @done 更新history.js内容
  cleanOldVersionFilesIfNeed (version) {
    let staticDir = this.staticDir
    let maxHistoryNum = this.maxHistoryNum

    let history = []

    try {
      history = require(this.historyFile)
    } catch (e) {
      console.log(e)
    }

    // 加入最新的版本,老的的版本删除
    history.push(version)

    // 如果历史版本数超过限制,则删除老的历史版本
    let len = history.length
    if (len > maxHistoryNum) {
      let oldVersions = history.slice(0, len - maxHistoryNum)

      for (let i = 0; i < oldVersions.length; i++) {
        let ver = oldVersions[i]
        let reg = new RegExp(ver)
        this.rmFiles(staticDir, reg)
      }

      // 更新history文件
      let newVersions = history.slice(len - maxHistoryNum)
      this.storeHistory(newVersions)
    } else {
      // 写入history文件
      this.storeHistory(history)
    }
  },

  // 入口
  run (version) {
    this.creataHistoryIfNotExist()
    this.cleanOldVersionFilesIfNeed(version)
  }
}

run.sh

我们简单的分为beta(测试环境)和pro(线上环境)

作用:一键打包本地代码,提交上传至远程代码仓库

先看代码:

#!/bin/bash

# 初始化上下文。有参数的
initContext(){
	# dist目录
	source_dir=dist

	# 获取参数,beta环境和pro环境
	if [ $# -gt 0 ] && [ $1 = 'beta' ];then
		# beta环境远程仓库地址,
		git_url=git@beta/example.git

		# 本地根目录存放打包代码
		dest=".deploy/beta"

		# npm 的脚本名,对应package.json定义的script
		node_script=build_beta
	else
		# pro环境远程仓库地址
		git_url=git@pro/example.git

		# 本地根目录
		dest=".deploy/pro"

		# npm 的脚本名,对应package.json定义的script
		node_script=build_pro
	fi
}

# 初始化git目录,pull最新代码
init(){
	echo +++init start;

	if [ ! -d $dest ]; then
	  	git clone $git_url $dest
	fi

	# 记录现在的目录位置,pwd获取本地路径
	cur=`pwd`

  	# 进入git目录
  	cd $dest

  	# git checkout .
  	git add .
  	git stash

  	# reset为线上最新版本,要先pull一下再reset。
  	git pull origin master
  	git reset --hard origin/master

	# 然后再pull一下
	git pull origin master

	# 回到原来的目录
	cd $cur
	echo ---init end;
}

# 重置dist目录
resetDist(){
	echo +++resetDist start

	rsync -a --delete --exclude='.git' $dest/. ./dist

	echo ---resetDist end
}

# 构建
build(){
	echo +++build start
	npm run $node_script
	echo ---build end
}

# 检查是否成功
checkBuild(){
	if [[ ! -f $source_dir/index.html || ! -d $source_dir/static ]]; then
		echo error
	else
		echo ok
	fi
}

# 复制代码到$dest目录
cpCode(){
	echo +++cpCode start
	# 复制代码,所有文件包含隐藏文件
	rsync -r --delete --exclude='.git'  $source_dir/. $dest

	echo ---cpCode end
}

# 提交到远程git仓库
commit(){
	echo +++commit start
	# 记录现在的目录位置,最后要回来的
	cur=`pwd`

	# 进入git目录
	cd $dest
	# 提交的字符串
	commit_str="commited in `date '+%Y-%m-%d_%H:%M:%S'`"

	git add .
	git commit -am "${commit_str}"
	git push origin master

	# 回到原来的目录
	cd $cur
	echo ---commit end
}

# 显示帮助信息
help(){
	# 微信h5版本
	echo ----微信h5版本--------
	echo ./run.sh build			"#"构建代码
	echo ./run.sh init			"#"初始化git仓库
	echo ./run.sh commit		"#"提交到git
	echo ./run.sh	 			"#"执行全部任务
	echo ./run.sh hello			"#"hello
	echo ./run.sh test			"#"test

	echo ./run.sh beta			"#"一键构建和提交beta版本
	# app内嵌版本
	echo ----app内嵌版本--------
	echo ./run.sh app			"#"一键构建和提交app版本

	echo ----帮助信息--------
	echo ./run.sh help			"#"帮助
}

# 测试用的
test(){
	echo "a test empty task"
}

# 入口
if [[ $# -lt 1  ||  $1 = 'app'  ||  $1 = 'beta' ]]; then
	# 无参数则打pro包,否则打相应类型的包
	if [ $# -lt 1 ];then
		type=pro
	else
		type=$1
	fi

	echo ===\>准备构建${type}版
	initContext $type && init && resetDist

	# 构建代码
	buildRes=$(build)

	# 检查构建结果
	echo -e "$buildRes"

	if [[ $buildRes =~ "ERROR" ]]; then
		echo "$(tput setaf 1)xxx\>build error,task abort$(tput sgr0)"
	else
		# 代码构建成功才继续。
		checkRes=$(checkBuild)

		if [ $checkRes == "ok" ];then
		 	cpCode && commit
			echo "$(tput setaf 2)===\>task complete$(tput sgr0)"
		else
			echo "$(tput setaf 1)xxx\>build error,task abort$(tput sgr0)"
		fi
	fi


elif [ $1 ]; then
	# 参数不是包类型的,当中函数处理
	echo ===\>准备执行${1}函数
	initContext beta

	func=$1
	$func
	echo ===\>task complete
fi

上篇文章评论区有人问到 rsync ,文中用来同步本地硬盘中的不同目录。可以看下这篇 教程

Linux 系统已经集成了 rsync 命令,windows 的用户需要自行安装(教程),不然会和这位小伙伴一样遇到 command not found,本人windows已经3年没碰了...

以上,是对上篇文章中对打包这块内容的讲解,接下来说下部署

部署工具我接触过walle、Jenkins

walle的话就是提交对应版本进行发布,持续集成部署方案的话就是Jenkins了。Jenkins文档

为什么要做自动化部署?结合自身实际场景描述下:

  • 后端有微服务,前端自然不能落下。各个大模块(业务线)都是独立的工程,比如商城、教育、社区等等。当开发迭代一个版本时,可能就会涉及到A、B、C、D等几个项目,部署的痛苦就来了…我特么的一个个打包,然后再一个个的部署...这个过程可能半小时就过去了…效率低下又low,实力不允许这么做
  • 公司有局域网限制,如果在家手动部署,还需要连接VPN,内心是拒绝的
  • 我只想安心敲代码…别特么的动不动让我部署下,打断老子的思路...

当然,使用Jenkins持续集成方案,该装的环境还是一个都不能少的…这个也结合自身实际情况自行研究吧

本文也是描述下这个概念,各自实际情况各异,扔需自行研究

安装什么的自行文档吧(安装,插件,配置等),从新建任务开始

  1. 进入Jenkins系统,左侧新建任务

  2. 输入名称,选择构建一个自由风格的软件项目,确定

  3. 填写任务描述、源码仓库地址,分支默认master

  4. 配置触发器,Gitlab提交代码触发自动构建任务

  5. 如果需要构建时执行脚本,比如

    npm i
    npm run build
    

  6. 保存

很简单的几个配置步骤就能完成自动化部署任务,简单高效。自行研究尝试玩玩吧 下篇我再出个Jenkins+Github持续集成的详细教程好了

  1. 关于Jenkins持续集成

Jenkins持续集成网上已经有了很成熟的教程,感觉我没有必要再出篇类似的教程了,我fork了一份可做参考借鉴(可能Gitlab/Github版本不是最新的,部分内容略有差异,本人亲测问题不大)

Jenkins+Gitlab: 教程

Jenkins+Github:手把手教程

结尾

通过这样的方式打包部署个人认为是适合绝大多数中小公司的,哈哈。我本来考虑的是将css、js部署在cdn服务器上,index.html单独部署的,考虑到暂时还没到需要这么干的程度,就不折腾服务器了,思路大同小异。

在写这篇文章的时候,正好看到大公司里怎样开发和部署前端代码?,大家可以了解下他们是怎么设计的。我这中小公司,感觉文中的思路方案也差不多OK的啦

提前写了,本来说是周末写的,正好今天不忙,花2小时简单写下