第一章 本周导学
1-1 本周整体内容介绍和学习方法
- 云构建原理、架构和实现
- WebSocket入门到实战
- Redis入门实战
第二章 云架构模块架构设计
2-1 详细分析为什么需要设计云构建系统
为什么需要云构建
- 减少发布过程中的重复劳动
- 打包构建
- 上传静态资源服务器
- 上传CDN
- 避免不同环境造成的差异
- 提升构建性能
- 对构建过程进行统一管控
- 发布前代码统一规则检查
- 封网日统一发布卡口
2-2 云构建系统架构设计
第三章 WebSocket 快速入门
3-1 WebSocket基本概念及同HTTP协议对比
WebSocket概念
- HTTP:请求响应的单向。
- WebSocket:只需发起一次请求,双向发起请求,双向接收响应。常用为聊天工具、云构建请求。
- 客户端开发WebSocket与浏览器开发WebSocket是不同的。
- 如何通过NodeJs搭建一个WebSocket服务。
相关基本概念:https://www.runoob.com/html/html5-websocket.html
3-2 egg集成WebSocket服务
基础介绍
基础教程:https://eggjs.org/zh-cn/tutorials/socketio.html demo仓库:https://github.com/eggjs/egg-socket.io/tree/master/example
代码在lesson01分支基础上继续开发:cloudscope-cli-server/leeson01 在lesson01分支基础上,新建分支 lesson30,本周最终代码在lesson30分支上。
WebSocket服务开发流程
- 安装依赖
- cnpm i -S egg-socket.io
- 更新配置文件
// config.default.jsconfig.io = {namespace: {'/': {connectionMiddleware: ['auth'],packetMiddleware: ['filter'],},'/chat': {connectionMiddleware: ['auth'],packetMiddleware: [],},},};// plugin.jsexports.io = {enable: true,package: 'egg-socket.io',};
- 修改路由配置
// router.js// app.io.of('/')app.io.route('chat', app.io.controller.chat.index);// app.io.of('/chat')app.io.of('/chat').route('chat', app.io.controller.chat.index);
- 开发middleware
// app/io/middleware/auth.js'use strict';module.exports = () => {return async (ctx, next) => {const say = await ctx.service.user.say();ctx.socket.emit('res', 'auth!' + say);await next();console.log('disconnect!');};};//app/io/middleware/filter.js'use strict';module.exports = () => {return async (ctx, next) => {console.log(ctx.packet);const say = await ctx.service.user.say();ctx.socket.emit('res', 'packet!' + say);await next();console.log('packet response!');};};
- 开发controller
// app/io/controller/chat.js'use strict';module.exports = app => {class Controller extends app.Controller {async index() {const message = this.ctx.args[0];console.log('chat :', message + ' : ' + process.pid);const say = await this.ctx.service.user.say();this.ctx.socket.emit('res', say);}}return Controller;};
3-3 WebSocket客户端开发
代码分支在 cloudscope-cli/lesson30上。
- lerna create cloudbuild models/
- cd models/cloudbuild
- npm i -S socket.io-client
- cloudbuild/lib/index.js
'use strict';// or http://127.0.0.1:7001/chatconst socket = require('socket.io-client')('http://127.0.0.1:7001');socket.on('connect', () => {console.log('connect!');socket.emit('chat', 'hello world!');});socket.on('res', msg => {console.log('res from server: %s!', msg);});
3-4 WebSocket客户端与服务端交互流程分析
以日志的打出分析流程

- 首先服务端(cloudscope-cli-server)启动服务:npm run dev
- 客户端启动服务:node models/cloudbuild/lin/index.js
- 客户端启动后:
- socket on 连接成功,监控connect事件:打印出日志 connect!
- 接着客户端emit chat事件:socket.emit(‘chat’, ‘hello world!’);
- 服务端接收到chat事件后
- 首先会现在服务端config.default.js中找到 / 的 connectMiddleWare的 auth.js去执行
- auth.js中触发res事件
- 客户端监听res事件,打印日志:res from server:auth! Hello Man!!
- 接着服务端在config.default.js中 找到 / 的packetMiddleWare 的 filter.js去执行
- 服务端打印 ctx.packet日志:[‘chat’,’hello world!’]
- filter.js触发res事件
- 客户端监听res事件,打印日志:res from server:packet! Hello Man!!
- 服务端通过route匹配到 chat,于是去Controller中找 chat.js
- 服务端打印日志:chat:hello world! : 10826
- 接着服务端调用service服务,拿到值,触发 res事件
- 客户端监听到res事件,打印日志:res from server:Hello Man!!
- 服务端等待 next执行完毕后,最后在 filter.js中,打印出:packet response!
第四章 Redis 快速入门
4-1 redis基本概念+安装方法+基本命令
- 该项目应用redis是要:存储任务信息
- redis安装
- 常用命令redis-cli[进入终端服务]和redis-server[启动redis服务]
4-2 阿里云redis服务配置和远程连接方法讲解
我这里实在腾讯云领了一个月的redis免费试用版本,下面记录为课程的讲解,腾讯云相关redis见读书笔记。
- 购买完数据库后,第一个设置是白名单设置,0.0.0.0/0 如果不设置,会出现远程无法连接的问题
- 创建账号:使用默认账号或创建账号连接
- 连接成功后 AUTH
4-3 egg集成redis方法讲解
redis为使用本地
- 首先在npm官网上查看 egg-redis这个插件
- 在server安装:npm i egg-redis —save
- 根据npm官网上关于egg-redis的代码讲解,分别在plugin.js和config.default.js中添加相关代码。
- 添加一个新路由:/redis/test,并在project的controller中测试
- 添加 await app.redis.get(key)获取key值
async getRedis(){const { ctx, app } = this;const num = await app.redis.get('number')console.log(num)ctx.body = 'hello redis'}//config.defualt.jsconfig.redis = {client: {port: REDIS_PORT,host: REDIS_HOST,password:REDIS_PWD,db: 0,},//plugin.jsexports.redis = {enable: true,package: 'egg-redis',};
第五章 云构建初始化流程开发
5-1 CloudBuild类开发
根据第二章 架构图,本节主要代码为 CloudBuild类的创建与引用,最终传入git对象
3-3节已经创建了cloudbuild,修改这里的代码为
'use strict';class CloudBuild {constructor(git, props){console.log('cloudbuild',git)}// or http://127.0.0.1:7001/chat// const socket = require('socket.io-client')('http://127.0.0.1:7001');// socket.on('connect', () => {// console.log('connect!');// socket.emit('chat', 'hello world!');// });// socket.on('res', msg => {// console.log('res from server: %s!', msg);// });}module.exports = CloudBuild
在modes/git下引入@cloudscope-cli/cloudbuild,并新建publish方法
async publish(){await this.preparePublish()const buildCmd = ''const cloudBuild = new CloudBuild(this,{buildCmd})}async preparePublish(){console.log('preparePublish')}
在commands/publish/lib/index.js 文件中,接着上一周的 自动化提交代码,接着调用 git.publsih方法。
- 这样从 commands/publish/lib/index.js中调用 git.publish()方法
- 在 models/git/lib/index.js中开发publish方法,publish中会生成一个cloudbuild实例
- 这个cloudbild实例为我们在models下新建的一个包,这样本节就形成了一个闭环。
- 下节开始就是cloudbuild实例的开发,以及publish流程。
5-2 生成构建命令+构建命令检查开发
本节主要内容是,用于定制build命令,通过 —buildCmd参数,如果用户传入build命令,那么使用传入的build命令打包,如果不是则传入默认的打包命令。
- 首先在 core/cli/lib/index.js中传入 —buildCmd参数
program.command('publish').option('--refreshServer','强制更新远程Git仓库类型和token').option('--refreshOwner','强制更新远程Git仓库用户类型').option('--buildCmd <buildCmd>','构建命令').action(exec)
- 然后在commands/publish/lib/index.js中接收这个参数
class PublishCommand extends Command {init(){// 处理参数log.verbose('publish init',this._argv)this.options = {refreshServer:this._argv[0].refreshServer,refreshOwner:this._argv[0].refreshOwner,buildCmd:this._argv[0].buildCmd}}…………}
- 此时Git类接收并赋值该参数,然后在上节我们写的 [models/git/lib/index.js] publishPrepare中见检查这个命令
async preparePublish(){if(this.buildCmd){const buildCmdArray = this.buildCmd.split(' ')if(!Object.is(buildCmdArray[0],'npm') && !Object.is(buildCmdArray[0],'cnpm')){throw new Error('Build命令非法,必须使用npm或cnpm!')}}else{this.buildCmd = 'npm run build'}console.log(this.buildCmd)}
- 最后测试一下:
终端命令: cloudscope-cli publish —targetPath /Users/liumingzhou/Documents/imoocCourse/Web前端架构师/cloudscope-cli/commands/publish —buildCmd ‘anpm run build:prod’ 抛出异常:Build命令非法,必须使用npm或cnpm
5-3 通过CloudBuild创建WebSocket连接
我们在第三章的学习当中已经大略的知道了 前后端如何建立起socket连接,本节就是对服务端代码修改以及客户端代码开发-传递git.repo参数到服务端。
- 服务端
- 将与service有关的代码先删掉
- 将routes.js中与chat相关的修改为 build:app.io.route(‘build’, app.io.controller.build.index);
- 服务端学习的核心代码为:const query = socket.handshake.query
//app/controller/io/controller/build.js'use strict';module.exports = app => {class Controller extends app.Controller {async index() {console.log('Controller build!')}}return Controller;};//app/controller/io/middleware/auth.js'use strict';module.exports = () => {return async (ctx, next) => {const {socket,logger} = ctxconst query = socket.handshake.querylogger.info(query)console.log('connect!')await next();console.log('disconnect!');};};
- 客户端
- Cloudbuild实例的init方法传入了一个参数:git.remote
// models/cloudbuild/lib/index.js'use strict';const io = require('socket.io-client');const TIME_OUT = 5* 60const WS_SERVER = 'http://liugezhou.com:7001'class CloudBuild {constructor(git, options){this.git = gitthis.buildCmd = options.buildCmdthis.timeout = TIME_OUT}init(){const socket = io(WS_SERVER,{query:{repo:this.git.remote}})socket.on('connect', () => {console.log('connect!');// socket.emit('chat', 'hello world!');});}}
最后测试一下:
- 最后测试一下,将服务端启动,在客户端执行命令:
终端命令: cloudscope-cli publish —targetPath /Users/liumingzhou/Documents/imoocCourse/Web前端架构师/cloudscope-cli/commands/publish —buildCmd ‘npm run build:prod’
- 在客户端与服务端分别打印出日志,测试正确
5-4 WebSocket超时自动断开连接逻辑开发
本节比较简单,是解决连接不上服务端的问题—连接超时,抛出异常,部分代码如下:
const CONNET_TIME_OUT = 5*1000class CloudBuild {constructor(git, options){this.git = gitthis.buildCmd = options.buildCmdthis.timeout = TIME_OUT}doTtimeout(fn,timeout){this.timer && clearTimeout(this.timer)log.info('设置任务超时时间:',`${timeout/1000}秒`)this.timer = setTimeout(fn,timeout);}init(){const socket = io(WS_SERVER,{query:{repo:this.git.remote}})const disconnect = ()=>{clearTimeout(this.timer)socket.disconnect()socket.close()}this.doTtimeout(()=>{log.error('云构建服务连接超时,自动终止')disconnect()}, CONNET_TIME_OUT);}}
5-5 WebSocket客户端和服务端通信优化
在整个websocket连接成功之后,是可以发送一些标准化日志的。
- 友好实现socket连接后的标准化日志,服务端改造
- 第一个改造点,cloudscope-cli-server中auth.js添加try catch异常捕获
- 第二个改造点:新建app/extend/helper.js,统一的返回格式
// app/extend/helper.js'use strict';module.exports = {parseMsg(action,payload={},metadata={}){const meta = Object.assign({},{timestamp:Date.now()},metadata);return {meta,data:{action,payload}}}}// app/controller/io/middleware/auth.jsconst {socket,logger,helper} = ctxsocket.emit(id,helper.parseMsg('connect',{type:'connect',message:'云构建服务连接成功'}))
客户端
const get =require('lodash/get')function parseMsg(msg){const action = get(msg,'data.action')const message = get(msg,'data.payload.message')return {action,message}}socket.on('connect', () => {clearTimeout(this.timer)const {id} = socketsocket.on(id,msg=>{const parsedMsg = parseMsg(msg)log.success(parsedMsg.action,parsedMsg.message,`任务ID:${id}`)})});socket.on('disconnect',()=>{log.success('disconnect','云构建任务断开')disconnect();})socket.on('error',(err)=>{log.error('error','云构建错误',err)disconnect()})
5-6 云构建任务写入Redis
本节主要内容就是将云构建任务写入到redis中去【服务端】,核心代码如下:
// app/controller/io/middleware/auth.jsconst REDIS_PREFIX = 'cloudbuild'const {app} = ctxconst {redis} = app// 判断redis任务是否存在let hashTask = await redis.get(`${REDIS_PREFIX}:${id}`)if(!hashTask){await redis.set(`${REDIS_PREFIX}:${id}`,JSON.stringify(query))}hashTask = await redis.get(`${REDIS_PREFIX}:${id}`)logger.info(hashTask)
5-7 创建云构建任务功能开发
客户端添加build方法
// models/cloudbuild/lib/index.jsbuild(){return new Promise((resolve,reject)=>{this.socket.emit('build')this.socket.on("build",msg=>{const parsedMsg = parseMsg(msg)log.success(parsedMsg.action,parsedMsg.message)})this.socket.on('building',msg=>{const parsedMsg = parseMsg(msg)log.success(parsedMsg.action,parsedMsg.message)})//commands/publish/lib/index.js//publish方法中await cloudBuild.init()await cloudBuild.build()
在服务端,我们在已经建好的 app/controller/io/controller/build.js中
'use strict';const REDIS_PREFIX = 'cloudbuild'const CloudBuildTask = require('../../models/CloudBuildTask')async function createCloudBuildTask(ctx,app){const { socket,helper } = ctxconst { redis } = appconst client = socket.idconst redisKey = `${REDIS_PREFIX}:${client}`const redisTask = await redis.get(redisKey)const task = JSON.parse(redisTask)socket.emit('build',helper.parseMsg('create task',{message:'创建云构建任务'}))return new CloudBuildTask({repo:task.repo,name:task.name,version:task.version,branch:task.branch,buildCmd:task.buildCmd})}module.exports = app => {class Controller extends app.Controller {async index() {//创建云构建任务const {ctx,app} = thisconst cloudBuildTask = await createCloudBuildTask(ctx,app)}}return Controller;};// app/models/CloudBuildTask.js'use strict';class CloudBuildTask {constructor(options){console.log(options)}}module.exports = CloudBuildTask
测试,可在服务端打印出options。
第六章 云构建执行流程开发
6-1 云构建任务初始化流程开发
服务端云构建的初始化流程,主要内容为CloudBuildTask这个类
- npm i -S user-home simple-git fs-extra
// 注意CloudBuildTask类需要传入的第二个参数为ctx'use strict';const path = require('path')const fse = require('fs-extra')const userHome = require('user-home')const Git = require('simple-git')class CloudBuildTask {constructor(options,ctx){this._ctx = ctxthis._name = options.name //项目名称this._version = options.version //项目版本号this._repo = options.repothis._branch = options.branchthis._buildCmd = options.buildCmd //构建命令this.logger = this._ctx .logger// 服务器的用户主目录this._dir = path.resolve(userHome,'.cloudscope-cli','cloudbuild',`${this._name}@${this._version}`) //缓存目录this._sourceCodeDir = path.resolve(this._dir,this._name) //缓存源码目录this.logger.info('_dir',this._dir)this.logger.info('_sourceCodeDir',this._sourceCodeDir)}async prepare(){fse.ensureDirSync(this._dir)fse.emptyDirSync(this._dir)this._git = new Git(this._dir)}}module.exports = CloudBuildTask
6-2 云构建任务交互日志开发
本节在CloudBuildTask类中,还未进行开发前,现对错误日志,进行了升级或者说是友好的异常抛出。 首先在CloudBuildTask这个类中,对于返回的格式进行了统一
// app/models/CloudBuildTask.jsconst {SUCCESS,FAILED} = require('../constant') //定义了常量success(msg,data){return this.response(SUCCESS,msg,data)}fail(msg,data){return this.response(FAILED,msg,data)}response(code,message,data){return {code,message,data}
然后在 app/io/controller/build.js中。
async function prepare(cloudBuildTask,socket,helper) {socket.emit('build',helper.parseMsg('prepare',{message:'开始执行构建前的准备工作'}))const prepareRes = await cloudBuildTask.prepare()if(!prepareRes || Object.is(prepareRes.code,FAILED)){const msg = prepareRes ? prepareRes.message : 'prepare返回为undefined'socket.emit('build',helper.parseMsg('prepare failed',{message: `执行云构建准备工作失败,失败原因:${msg}`}))return}socket.emit('build',helper.parseMsg('prepare',{message:'构建前准备工作成功'}))}
然后在客户端的models/cloudbuild/lib/index.js,监听的build方法中做了一个优化
build(){return new Promise((resolve,reject)=>{this.socket.emit('build')this.socket.on("build",msg=>{const parsedMsg = parseMsg(msg)if(FAILED_CODE.indexOf(parsedMsg.action)>-1){log.error(parsedMsg.message)clearTimeout(this.timer)this.socket.disconnect()this.socket.close()}log.success(parsedMsg.action,parsedMsg.message)})this.socket.on('building',msg=>{const parsedMsg = parseMsg(msg)log.success(parsedMsg.action,parsedMsg.message)})})}
6-3 服务端源码下载 + 切换分支
在服务端 app/io/controller/build.js下,
- 我们完成了 await prepare(cloudBuildTask,socket,helper),
- 接着我们继续实现 await download(cloudBuildTask,socket,helper)
// app/io/controller/build.jsasync function download(cloudBuildTask,socket,helper){socket.emit('build',helper.parseMsg('download repo',{message:'开始下载源码'}))const downloadRes = await cloudBuildTask.download()if(!downloadRes || Object.is(downloadRes.code,FAILED)){ // downlod下载失败socket.emit('build',helper.parseMsg('download failed',{message:'源码下载失败'}))return}else{socket.emit('build',helper.parseMsg('download repo',{message:'源码下载成功'}))}}// app/models/CloudBuildTask.jsasync download(){await this._git.clone(this._repo) //clone仓库this._git = new Git(this._sourceCodeDir) //将git地址更改,生成新的simple git// 切换分支 git checkout -b dev/1.0.2 origin/dev/1.0.2await this._git.checkout(['-b',this._branch,`origin/${this._branch}`])return fs.existsSync(this._sourceCodeDir) ? this.success() : this.failed()}
然后在客户端执行命令:cloudscope-cli publish —targetPath /Users/liumingzhou/Documents/imoocCourse/Web前端架构师/cloudscope-cli/commands/publish
经过测试,在/User/username/.cloudscoe-cli/cloudbuild/test@1.0.2/test下安装了源码,执行git branch 看到切换到了 开发分支。
6-4 服务端源码依赖安装+命令执行功能封装
与上一节的download流程一样
// app/io/controller/build.jsawait install(cloudBuildTask,socket,helper)async function install(cloudBuildTask,socket,helper) {socket.emit('build',helper.parseMsg('install',{message:'开始安装依赖'}))const installRes = await cloudBuildTask.install()if(!installRes || Object.is(installRes.code,FAILED)){ // downlod下载失败socket.emit('build',helper.parseMsg('install failed',{message:'安装依赖失败'}))return}else{socket.emit('build',helper.parseMsg('install',{message:'安装依赖成功'}))}}// app/models/CloudBuildTask.jsasync install(){let res = trueres && (res = await this.execCmd('npm install --registry=https://registry.npm.taobao.org'))return res ? this.success(): this.failed()}execCmd(cmd){// npm install ->['npm','install']let command = cmd.split(' ')if(command.length === 0){return null}const firstCommand = command[0]const leftCommand = command.slice(1) || []return new Promise((resolve,reject)=>{const p = exec(firstCommand,leftCommand,{cwd:this._sourceCodeDir},{stdio:'pipe'})p.on('error',e=>{this._ctx.logger.error('build error',e)resolve(fasle)})p.on('exit',c=>{this._ctx.logger.info('build exit',c)resolve(true)})p.stdout.on('data',data => {this._ctx.socket.emit('building',data.toString())})p.stderr .on('data', data =>{this._ctx.socket.emit('building',data.toString())})})}function exec(command,args,options){const win32 = process.platform === 'win32';const cmd = win32 ? 'cmd': commandconst cmdArgs = win32 ? ['/c'].concat(command,args) : args;return require('child_process').spawn(cmd, cmdArgs,options || {})}
6-5 云构建任务执行逻辑开发
// app/io/controller/build.jsawait build(cloudBuildTask,socket,helper)async function build(cloudBuildTask,socket,helper) {socket.emit('build',helper.parseMsg('build',{message:'开始启动云构建'}))const buildRes = await cloudBuildTask.build()if(!buildRes || Object.is(buildRes.code,FAILED)){ // downlod下载失败socket.emit('build',helper.parseMsg('build failed',{message:'云构建任务执行失败'}))return}else{socket.emit('build',helper.parseMsg('build',{message:'云构建任务执行成功'}))}}// app/models/CloudBuildTask.jsasync build(){let res = trueif(checkCommand(this._buildCmd)){res = await this.execCmd(this._buildCmd)}else{res = false}return false ? this.success():this.failed()}function checkCommand(command){if(command){const commands = command.split(' ')if(commands.length === 0 || ['npm','cnpm'].indexOf(commands[0])<0){return false}return true}return false}
最后在客户端经过测试,看到 build的dist目录没有构建成功:这是因为我的test源码不是一个可以打包的项目。 于是重写创建项目,并且publish 的时候输入参数 —refreshServer —refreshOwner ,构建成功。
