拋棄jenkins,如何用node從零搭建自動化部署管理平臺

1、背景

H5頁面因爲其具備發佈靈活、跨平臺、易於傳播等突出特色,因此H5頁面是引流拉新、宣傳推廣的重要渠道和方式,備受各公司的青睞。html

小編的平常工做就是作各類面向用戶的H5促銷活動的開發,在整個開發週期中,接合我司的一些狀況,我總結了H5活動頁面的如下特色:前端

  1. 面向用戶,流量大;
  2. 各端展現方案不一樣,須要兼容各端(好比活動規則、展現模塊,ios和android不同);
  3. 需求變動頻繁;
  4. 合做方較多(須要跟各個業務線合做聯調);
  5. 排期緊張;

因此開發測試期間,部署效率就顯得特別重要了。node

因爲我司的CDN發佈平臺,須要手動建立模板、粘貼代碼,部署效率比較低下;而且活動頁面代碼分散,沒法統一管理和實現工程化,因此決定實現一套自動化部署系統,目前已經投入使用半年時間了,極大地提升了咱們的工做效率。我稱這個自動化部署系統爲【H5 活動管理平臺】android

2、H5 活動管理平臺自動化部署實現方案

介紹該平臺實現方案以前,先放張效果圖,好有一個直觀的認識。webpack

H5活動管理平臺界面

該平臺實現主要依賴於本地開發工程gitlab,三者之間經過通訊交互,實現的自動化部署。ios

image

最終達到的效果就是:當本地開發分支merge到測試分支devTest或者master分支時,該平臺會自動拉取最新代碼,構建目標文件,而後將目標文件部署到對應的服務器目錄,另外提供了上下線、版本回滾、定時上下線等經常使用功能。git

總體架構流程圖:web

H5活動管理平臺架構流程圖

下面對一些關鍵技術點進行詳細介紹shell

1. 本地開發工程

咱們的本地開發工程,是使用node + webpack + babel等相關技術搭建的多頁面開發工程,不一樣的活動位於不一樣的目錄。由於要作自動化構建部署處理,跟【H5活動管理平臺】交互,因此有如下要點須要注意(可根據本身項目狀況,自由調整方案)。npm

  1. 本地開發工程做爲自動化構建部署的源頭,須要提供構建命令行用於構建測試文件和線上文件,便於後面shell命令調用。如在package.json中加入以下命令:
"scripts": {
    "local": "cross-env NODE_ENV=local node build.js", // 本地開發命令
    "build": "cross-env NODE_ENV=product node build.js", // 構建上線文件
    "test": "cross-env NODE_ENV=test node build.js" // 構建測試文件
}
複製代碼
  1. 提供構建配置文件dev-config.js,用於過濾webpack構建時的入口目錄,只構建編譯當前正在開發的活動頁面,提升構建速度。
//dev-config.js
module.exports = {
    devPages: ['test']   //  當前本身正在開發頁面目錄,不寫時會編譯全部活動頁面
}
複製代碼
  1. 提供活動頁面目錄信息配置config.json,該配置信息用於【H5活動管理平臺】的展現,也就是效果圖中的信息源。
// config.json
{
  "pages": [
        {
            "folder": "lion",
            "desc": "前端名獅",
            "author": "訣九",
            "user": "juejiu"
        },
        {
            "folder": "test",
            "desc": "活動測試頁面",
            "author": "訣九",
            "user": "juejiu"
        }
    ]
}
    
複製代碼
  1. 構建生成的 JSHTML 文件,存放在 dist 目錄下的對應活動目錄中。構建生成的目錄結構以下:
|--dist
   |-- lion
       |-- lion_app.js
       |-- index.html
   |--test
       |-- test_app.js
       |-- index.html

複製代碼
  1. 提測時,將開發分支merge到devTest分支,上線時,將開發分支merge到master分支。

工程目錄結構

2. gitlab服務器

Gitlab做爲企業代碼版本管理工具,提供了Webhook的功能配置,Webhook顧名思義,其實就是一鉤子。當咱們在Gitlab上作出某些特定操做時,能夠觸發鉤子,去進行一些咱們事先設定好的腳本,以達到某些特定功能(例如--前端項目自動發佈)。

實際上能夠把它理解爲回調,或者委託,或者事件通知,歸根揭底它就是一個消息通知機制。當gitlab觸發某個事件時,它會向你的所配置的http服務發送Post請求

注意:

  1. URL處填寫的是【H5活動管理平臺】部署的服務器IP;
  2. IP後面跟的merge是該平臺提供的一個接口,用於觸發鉤子後,gitlab服務器向這個接口發送Post請求;
  3. Secret Token處填寫的是一個token,主要用於merge接口請求作安全校驗,能夠隨便設置。

具體配置以下圖:

webhooks 配置

咱們項目是設置的merge鉤子,下面只貼一下Merge request events請求傳遞的數據信息:

Request header:

X-Gitlab-Event: Merge Request Hook
複製代碼

Request body:

{
  "object_kind": "merge_request",
  "user": {
    "name": "Administrator",
    "username": "root",
    "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
  },
  "object_attributes": {
    "id": 99,
    "target_branch": "master",
    "source_branch": "ms-viewport",
    "source_project_id": 14,
    "author_id": 51,
    "assignee_id": 6,
    "title": "MS-Viewport",
    "created_at": "2013-12-03T17:23:34Z",
    "updated_at": "2013-12-03T17:23:34Z",
    "st_commits": null,
    "st_diffs": null,
    "milestone_id": null,
    "state": "opened",
    "merge_status": "unchecked",
    "target_project_id": 14,
    "iid": 1,
    "description": "",
    "source":{
      "name":"Awesome Project",
      "description":"Aut reprehenderit ut est.",
      "web_url":"http://example.com/awesome_space/awesome_project",
      "avatar_url":null,
      "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
      "git_http_url":"http://example.com/awesome_space/awesome_project.git",
      "namespace":"Awesome Space",
      "visibility_level":20,
      "path_with_namespace":"awesome_space/awesome_project",
      "default_branch":"master",
      "homepage":"http://example.com/awesome_space/awesome_project",
      "url":"http://example.com/awesome_space/awesome_project.git",
      "ssh_url":"git@example.com:awesome_space/awesome_project.git",
      "http_url":"http://example.com/awesome_space/awesome_project.git"
    },
    "target": {
      "name":"Awesome Project",
      "description":"Aut reprehenderit ut est.",
      "web_url":"http://example.com/awesome_space/awesome_project",
      "avatar_url":null,
      "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
      "git_http_url":"http://example.com/awesome_space/awesome_project.git",
      "namespace":"Awesome Space",
      "visibility_level":20,
      "path_with_namespace":"awesome_space/awesome_project",
      "default_branch":"master",
      "homepage":"http://example.com/awesome_space/awesome_project",
      "url":"http://example.com/awesome_space/awesome_project.git",
      "ssh_url":"git@example.com:awesome_space/awesome_project.git",
      "http_url":"http://example.com/awesome_space/awesome_project.git"
    },
    "last_commit": {
      "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
      "message": "fixed readme",
      "timestamp": "2012-01-03T23:36:29+02:00",
      "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
      "author": {
        "name": "GitLab dev user",
        "email": "gitlabdev@dv6700.(none)"
      }
    },
    "work_in_progress": false,
    "url": "http://example.com/diaspora/merge_requests/1",
    "action": "open",
    "assignee": {
      "name": "User1",
      "username": "user1",
      "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
    }
  }
}

複製代碼

3. H5 活動管理平臺

當開發者merge代碼到GitLab服務器,會觸發merge事件,GitLab會發送一個POST請求連帶數據(數據格式)給webhooks指定的URL,該平臺接收到URL請求後,就涉及以下關鍵技術點:

1. 根據post請求頭信息和和body數據,咱們能獲得以下信息:

merge的目標分支: req.body.object_attributes.target_branch

安全校驗token:req.headers['x-gitlab-token']

gitlab工程倉庫地址:req.body.project.git_ssh_url

觸發的鉤子行爲類型:req.body.object_attributes.action

// gitlab觸發merge請求
router.post('/merge', function (req, res, next) {
    let git_ssh_url = req.body.project.git_ssh_url;
    let name = req.body.project.name;
    // 上線merge分支master
    if (req.headers['x-gitlab-token'] == 'mergeRequest' && req.body.object_attributes.target_branch == 'master' && req.body.object_attributes.action == 'merge') {
        if (config[name] && config[name].git_ssh_url == git_ssh_url) {
            mergeTaskQueue.addTask(function () {
                getCode.init(git_ssh_url, name, 'master').then(function (data) {
                    console.log(data);
                    mergeTaskQueue.run();
                }).catch(function (error) {
                    console.log(error);
                    mergeTaskQueue.run();
                })
            }.bind(null, git_ssh_url, name));
        }
        res.end('receive request');
        // 測試merge分支dev
    } else if (req.headers['x-gitlab-token'] == 'mergeRequest' && req.body.object_attributes.target_branch == config[name].testEnv.targetBranch && req.body.object_attributes.action == 'merge') {
        if (config[name] && config[name].git_ssh_url == git_ssh_url) {
            mergeTaskQueue.addTask(function () {
                getCode.init(git_ssh_url, name, req.body.object_attributes.target_branch).then(function (data) {
                    console.log(data);
                    mergeTaskQueue.run();
                }).catch(function (error) {
                    console.log(error);
                    mergeTaskQueue.run();
                })
            }.bind(null, git_ssh_url, name));
        }
        res.end('receive request');
    } else {
        return res.end('receive request');
    }
})
複製代碼

2. 執行腳本

腳本這塊沒有使用shell腳本,而是使用了node版本的shell.js庫,這個庫可讓咱們控制執行邏輯,更友好的處理錯誤信息,幫助平臺有更友好的信息展現。

拉取最新代碼進行構建出目標文件,大體邏輯以下圖:

目標代碼

function init(git_ssh_url, projectName, targetBranch) {
    deferred = Q.defer();
    if (!git_ssh_url || !projectName) {
        return deferred.reject('項目地址或者項目名稱爲空');
    }
    repository = git_ssh_url;
    repositoryName = projectName;
    clonePath = path.join(__dirname, '../projects/' + projectName);

    shell.exec('exit 0');
    if (shell.test('-e', clonePath)) {
        shell.cd(clonePath);
        let currentBranch = shell.exec('git symbolic-ref --short -q HEAD', {async: false, silent: true}).stdout;
        if(currentBranch != targetBranch) {
            let outInfo = shell.exec('git branch', {async: false, silent: true}).stdout;
            let gitcmd = outInfo.indexOf(targetBranch) >= 0 ? ('git checkout ' + targetBranch) : ('git checkout -b ' + targetBranch + ' origin/' + targetBranch);
            shell.exec('git pull && ' + gitcmd, {async: false, silent: true});
        }
        shell.exec('git pull', {async: false, silent: true}, function (code, stdout, stderr) {
            if (code != 0) {
                console.log(stderr);
                return deferred.reject('git pull error');
            }
            console.log(stdout);
            console.log('git pull run success');
            return buildTest(projectName, targetBranch);
        })
    } else {
        if (!fs.existsSync(projects_path)) {
            fs.mkdirSync(projects_path);
        }
        shell.cd(projects_path);
        shell.exec('git clone ' + repository, function (code, stdout, stderr) {
            if (code != 0) {
                console.log(stderr);
                return deferred.reject('git clone error');
            }
            console.log('git clone success');
            shell.cd(clonePath);
            let outInfo = shell.exec('git branch', {async: false, silent: true}).stdout;
            let gitcmd = outInfo.indexOf(targetBranch) >= 0 ? ('git checkout ' + targetBranch) : ('git checkout -b ' + targetBranch + ' origin/' + targetBranch);
            shell.exec(gitcmd, {async: false, silent: true});
            return buildTest(projectName, targetBranch);
        })
    }
    return deferred.promise;
}

// 構建項目
function buildTest(projectName, targetBranch) {
    shell.cd(clonePath);
    shell.exec('npm config set registry https://registry.npm.taobao.org && npm install', {async: true, silent: true}, function (code, stdout, stderr) {
        if (code != 0) {
            console.log(stderr);
            return deferred.reject('npm install error');
        }
        console.log('npm install success');
        shell.rm('-rf', path.join(clonePath, 'dist'));
        let testCommand = config[repositoryName].commands.test || 'npm run test'; //構建測試文件命令行
        shell.exec(testCommand, {async: true, silent: true}, function (code, stdout, stderr) {
            if (code != 0) {
                console.log(stderr);
                return deferred.reject('npm run test fail');
            }
            console.log('npm run test success');
            copyPage(repositoryName, 'test'); // copy到測試目錄
            if(targetBranch != 'master') {
                shell.exec('exit 0');
                deferred.resolve('build success and finish');
                return; // 提測時只構建測試文件
            }
            // 構建最終上線文件
            shell.rm('-rf', path.join(clonePath, 'dist'));
            let buildCommand = config[repositoryName].commands.build || 'npm run build'; //構建預上線文件命令行
            shell.exec(buildCommand, {async: true, silent: true}, function (code, stdout, stderr) {
                if (code != 0) {
                    console.log(stderr);
                    return deferred.reject('npm run build fail');
                }
                console.log('npm run build success');
                copyPage(repositoryName, 'online'); //copy到上線正式目錄
                
                // 每次合併master構建後,都切換到測試分支,便於平臺讀取config.json信息(測試分支是最新的)
                shell.exec('git checkout ' + config[projectName].testEnv.targetBranch, {async: false, silent: false}); 
                shell.exec('exit 0');
                deferred.resolve('build success and finish');
            })
        })
    })
}
複製代碼

3. 動態擴展項目

經過修改項目配置文件,接入不一樣的項目,配置信息有每一個項目要上傳的CDN路徑、構建命令、項目目錄展現信息文件路徑(config.json),以下圖:

// 接入該平臺的項目列表
module.exports = {
    'h5-activity-cms': {
        git_ssh_url: 'git@example.com:awesome_space/awesome_project.git',
        desc: '前端名獅項目',
        tabContent: '前端名獅', //頁面中tab展現文字
        onlineParam: { //上傳cdn的參數,根據本身項目設置
            html: {
                domain: '',
                path: ''
            },
            js: {
                domain: '',
                path: ''
            }
        },
        commands: { //構建腳本命令行
            test: 'npm run test',
            build: 'npm run build'
        },
       
        configFile: 'config.json', // 活動頁面列表信息
    }
}
複製代碼

4. 隊列處理

構建目標文件的過程當中,不少生成文件、壓縮、copy的異步操做,不一樣的merge請求,有可能操做的是同一個文件,因此須要對merge請求作隊列處理。

class TaskQueue {
    constructor() {
        this.list = [];
        this.isRunning = false;
    }
    addTask(task) {
        this.list.push(task);
        if(this.isRunning) {
            return;
        }
        this.start();
    }
    shift() {
        return this.list.length > 0 ? this.list.shift() : null;
    }
    run() {
        let task = this.shift();
        if(!task) {
            this.isRunning = false;
            return;
        }
        task();
    }
    start() {
        this.isRunning = true;
        this.run();
    }
}
module.exports = TaskQueue;
複製代碼

5. CDN 發佈

這個須要後端同窗提供一個服務接口,用於推送文件到CDN上或者服務器上。咱們這邊是藉助於一個服務端接口,咱們經過node上傳到他們的服務器,接口方會定時推送文件到CDN,具體每一個人的狀況處理吧哈。


3、總結

該平臺使用node實現了一個微型的、相似jenkins功能的部署管理平臺,具備以下突出的優勢:

  1. 該平臺打通了本地開發環境和測試環境部署,實現了測試部署自動化,節省了人工上傳粘貼代碼的時間,大大地提升了工做效率;

  2. 基於項目工程劃分的類別,便於開發者高效率的查找頁面;

  3. 支持動態擴展,能夠經過添加配置文件,接入其餘gitlab項目;

  4. 能夠根據須要定製化平臺操做頁面,比使用jenkins更靈活,更輕便;


掃一掃 關注個人公衆號【前端名獅】,更多精彩內容陪伴你!

【前端名獅】
相關文章
相關標籤/搜索