H5頁面因爲其具備發佈靈活、跨平臺、易於傳播等突出特色,因此H5頁面是引流拉新、宣傳推廣的重要渠道和方式,備受各公司的青睞。html
小編的平常工做就是作各類面向用戶的H5促銷活動的開發,在整個開發週期中,接合我司的一些狀況,我總結了H5活動頁面的如下特色:前端
因此開發測試期間,部署效率就顯得特別重要了。node
因爲我司的CDN發佈平臺,須要手動建立模板、粘貼代碼,部署效率比較低下;而且活動頁面代碼分散,沒法統一管理和實現工程化,因此決定實現一套自動化部署系統,目前已經投入使用半年時間了,極大地提升了咱們的工做效率。我稱這個自動化部署系統爲【H5 活動管理平臺】。android
介紹該平臺實現方案以前,先放張效果圖,好有一個直觀的認識。webpack
該平臺實現主要依賴於本地開發工程、gitlab,三者之間經過通訊交互,實現的自動化部署。ios
最終達到的效果就是:當本地開發分支merge到測試分支devTest或者master分支時,該平臺會自動拉取最新代碼,構建目標文件,而後將目標文件部署到對應的服務器目錄,另外提供了上下線、版本回滾、定時上下線等經常使用功能。git
總體架構流程圖:web
下面對一些關鍵技術點進行詳細介紹shell
咱們的本地開發工程,是使用node + webpack + babel
等相關技術搭建的多頁面開發工程,不一樣的活動位於不一樣的目錄。由於要作自動化構建部署處理,跟【H5活動管理平臺】交互,因此有如下要點須要注意(可根據本身項目狀況,自由調整方案)。npm
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" // 構建測試文件
}
複製代碼
dev-config.js
,用於過濾webpack
構建時的入口目錄,只構建編譯當前正在開發的活動頁面,提升構建速度。//dev-config.js
module.exports = {
devPages: ['test'] // 當前本身正在開發頁面目錄,不寫時會編譯全部活動頁面
}
複製代碼
config.json
,該配置信息用於【H5活動管理平臺】的展現,也就是效果圖中的信息源。// config.json
{
"pages": [
{
"folder": "lion",
"desc": "前端名獅",
"author": "訣九",
"user": "juejiu"
},
{
"folder": "test",
"desc": "活動測試頁面",
"author": "訣九",
"user": "juejiu"
}
]
}
複製代碼
JS
和 HTML
文件,存放在 dist
目錄下的對應活動目錄中。構建生成的目錄結構以下:|--dist
|-- lion
|-- lion_app.js
|-- index.html
|--test
|-- test_app.js
|-- index.html
複製代碼
Gitlab
做爲企業代碼版本管理工具,提供了Webhook
的功能配置,Webhook
顧名思義,其實就是一鉤子。當咱們在Gitlab
上作出某些特定操做時,能夠觸發鉤子,去進行一些咱們事先設定好的腳本,以達到某些特定功能(例如--前端項目自動發佈)。
實際上能夠把它理解爲回調,或者委託,或者事件通知,歸根揭底它就是一個消息通知機制。當gitlab觸發某個事件時,它會向你的所配置的http服務發送Post請求。
注意:
merge
是該平臺提供的一個接口,用於觸發鉤子後,gitlab服務器向這個接口發送Post請求;Secret Token
處填寫的是一個token,主要用於merge接口請求作安全校驗,能夠隨便設置。具體配置以下圖:
咱們項目是設置的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"
}
}
}
複製代碼
當開發者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,具體每一個人的狀況處理吧哈。
該平臺使用node實現了一個微型的、相似jenkins功能的部署管理平臺,具備以下突出的優勢:
該平臺打通了本地開發環境和測試環境部署,實現了測試部署自動化,節省了人工上傳粘貼代碼的時間,大大地提升了工做效率;
基於項目工程劃分的類別,便於開發者高效率的查找頁面;
支持動態擴展,能夠經過添加配置文件,接入其餘gitlab項目;
能夠根據須要定製化平臺操做頁面,比使用jenkins更靈活,更輕便;
掃一掃 關注個人公衆號【前端名獅】,更多精彩內容陪伴你!