介紹這個工具前不得不先介紹一下積木系統。html
積木系統是 imweb 團隊出品、爲產品運營而生的一套活動頁面發佈系統,詳細介紹見 PPT前端
簡單能夠這麼理解它的理念:python
一個頁面 = 一個模板 + 多個組件webpack
一個組件 = 一份代碼 + 一份數據nginx
一個組件開發一次,複用屢次git
一個頁面使用多個組件拼裝後,實時預覽、快速發佈上線github
此前在阿里實習的時候也接觸過一個叫 TMS(淘寶內容管理系統)的系統, 專門用於快速搭建電商運營活動頁面.web
這種系統可統一理解爲運營活動頁面發佈系統。apache
這種系統有如下特色:json
靜態數據或輕後臺數據(輕量 CGI)
單頁(多圖、圖文混合偏多)
組件粒度小,可靈活拼裝頁面
活動頁面須要快速發佈上線
積木系統已經經受了多個項目的考驗,目前也啓動了 2.0 的開發計劃, 做者 @江源 也曾在 PPT 中提到有開源的計劃,你們能夠期待一下。
在這裏我寫了一套相似的 Pager 系統,設計理念大同小異,只不過是想嘗試用新的技術棧快速實現。
項目地址是: https://github.com/laispace/pager
安裝環境比較麻煩,先來快速預覽下它的功能。
建立一個頁面, 添加可複用的組件,進行可視化編輯:
設置頁面信息:
生成頁面,可本地下載預覽:
發佈上線,同步到遠程機器:
接下來,直接訪問 http://pages.laispace.com/demo-page2/ 就能夠看到發佈的頁面了。
當我把原型寫出來的時候我卻發現,ES6 和 React 帶來的一系列特性,讓我以爲代碼寫起來爽到飛起,因此給你們分享下有趣的東西。
目前這個代號爲 Pager 的系統只實現了簡單的 組件編譯/頁面生成/頁面發佈 的功能, 還不能用於生產環境.
因此本文先給你們介紹下設計思路 :( 項目完成後, 再給你們細細介紹它的實現.
這個流程的角色主要對應是產品運營經理, 因此操做必須簡單.
新建頁面, 配置頁面基礎信息(標題/分享信息等)
在頁面中添加組件並配置組件數據(實時預覽/頁面大小可拖拽)
新窗口打開預覽頁面(預覽效果就是生成後的頁面,須要與線上發佈版本一致)
下載頁面到本地(不使用一鍵發佈, 自行下載代碼使用其餘系統發佈)
發佈頁面到服務器(一鍵發佈, 需保證服務器配置好了對應目錄的訪問權限)
這個流程的角色主要對應是前端開發, 須要保證開發模式足夠舒暢.
新建組件, 編寫組件代碼
打開組件預覽頁面
修改組件配置和代碼
監聽修改, 實時預覽更新
開發完成,同步到系統中(從新編譯, 覆蓋上一個版本)
系統承載多個項目, 項目中配置歸屬這個項目的頁面在發佈時的一些配置信息.
一個頁面由多個組件構成, 每一個組件爲一個文件夾, 組件間相互獨立, 本地開發完成後, 編譯並導入到系統中.
注意:綠色爲已有功能, 目前只提供了頁面建立相關功能, 尚未鑑權/版本控制等模塊, 因此還不能用於生產環境.
雖然先後端都本身寫, 能夠採用本身喜歡的接口方式. 但考慮到語義化和拓展性, 仍是建議使用先後端分離的 restful 接口形式.
一個名詞對應一個資源, 一個動詞對應一個操做:
增長一個組件, POST /components/
刪除一個組件, DELETE /components/:Id
查找全部組件, GET /components/
查找一個組件, GET /compnents/:Id
修改一個組件, PUT /components/:Id
先後端通訊是 JSON 數據格式, 同時使用 mongoose 定義一些數據模型, 方便快速地增刪查改, 創建項目原型.
像嵌套比較深的數據, 有時咱們並不想定義太多, 那直接用一個 Mixed 類型就能夠解決, 好比一個頁面中包含多個組件, 每一個組件實際上是有本身的數據格式的, 我這裏並不想用兩張表來存儲(相似外鍵), 因此直接在一個頁面下就存儲了這個頁面須要的全部數據:
import mongoose from 'mongoose'; const Schema = mongoose.Schema; const schema = new mongoose.Schema({ name: String, description: String, components: [Schema.Types.Mixed], // 組件, 混合的數據格式 project: String, config: Object });
頁面信息(title + meta + link + script)
一個 html 頁面, 從上往下是:
title 頁面標題
meta 頁面元信息
link/style 外聯或內聯樣式(自定義樣式方便快速修復UI問題而不須要從新發布代碼版本)
script 外聯或內聯腳本(自定義腳本方便快速添加上報點等非固話的操做)
多個模塊(component + data)
每一個組件都有本身的模板, 對應一套數據, 遵循組件粒度化,一個模板套一份數據的原則.
發佈配置(publishIp+publishDir+rsync)
不一樣的項目下生成不一樣的頁面, 最終使用 rsync 將頁面目錄同步到遠程機器, 遠程機器使用 nginx/apache 配置下代理, 就實現了頁面發佈.
注意: rsync 權限, 建議在遠程服務器上建立對應的目錄, 給予 rsync 帳戶只能訪問這個目錄, 以避免帶來沒必要要的安全問題.
這個項目使用 React+ES6 寫的, 和你們分享一些當心得.
我對 React 最重要的理解是單向的自頂向下的組件嵌套和數據流動, 帶來了數據的一致性保障. 對於一些不是很是複雜的單頁應用, 其實一個頁面就是一個組件, 不須要用太多的 flux/redux 等方案也足矣.
state = {name: 'simple', age: 18} addAge = () => { this.setState({ age: this.state.age++ }) } render : () => { return ( <div> 名字:<div>{this.state.name} </div> 年齡:<div>{this.state.age} </div> <button onClick={this.addAge}>點擊加一歲</button> </div> ) }
ES6 帶來了很是多的特性, 我在使用的過程當中感受比較好玩的是如下幾個.
import 帶來真正的模塊化
async/await 同步方式寫異步代碼
@decorator 無侵入的裝飾器
()=>{} 箭頭函數簡化代碼、保留 this 做用域
babel+webpack 爲新特性保駕護航
模塊化的方案, 之前有 AMD/CMD 甚至是 UMD, 趕上不一樣的項目就能夠用到不一樣的模塊化方案, 天然帶有不一樣的學習成本.
ES6 提供的 import/export 帶來的是更舒暢的模塊化, 就像在寫 python 同樣, 一個文件就是一個模塊, 純粹.
有了 babel 將 ES6 無縫地轉化爲 ES5 代碼後, 我以爲若是不考慮轉化後的代碼體積偏大的問題, 咱們在項目中就應該擁抱 ES6.
若是須要兼容之前的 AMD/CMD 模塊, 配上 webpack 使用便可.
// 導入所有 import path from 'path'; import Component from '../models/component'; // 導入局部 import { getComponent, getComponents } from '../utils/resources';
是異步的操做就應該使用 promise, 配合 ES7 的 async/await 語法糖, 舒服地編寫同步的代碼風格表示異步的操做, 爽.
首先須要定義多個異步操做,返回 Promise:
const findOnePage = (pageId) => new Promise((resolve, reject) => { Page.findOne({_id: pageId}).then(page => { resolve(page); }); }); const findOneProjectByName = (name) => new Promise((resolve, reject) => { Project.findOne({name: name}).then(project => { resolve(project); }); });
接着使用 await 獲取異步操做的結果:
const page = await findOnePage(pageId); const project = await findOneProjectByName(page.project);
能夠看到, 在使用 async/await 時, 少了回調, 少了嵌套, 代碼更加易讀. 固然這裏的代價是咱們須要封裝好供 await 使用的 promise(我以爲這裏仍是挺麻煩的), 不過咱們再也看不到回調地獄了, 咱們甚至能夠不使用 yield/generator 而直接過渡到 async/await 了.
ES7? ES6 都沒普及, 你 TM 叫我用 ES7?
這不是有 babel 嘛~ 用吧!
裝飾器其實也就是一個語法糖, 嘗試這麼理解: 咱們有 A/B/C 三個函數分別作了三個操做, 如今假設咱們忽然想在這些函數裏頭打印一些東西.
去改動三個函數固然能夠, 但更好的方式是定一個一個 @D 裝飾器, 裝飾到三個函數前面, 這樣他們除了執行原有功能外, 還能執行咱們注入進去的操做.
好比我在項目中, 不一樣的頁面都須要用到 snackbar(操做提示框), 每一個頁面都是同樣的, 沒有必要在每一個頁面都寫同樣的代碼, 只須要將這個組件以及對應的方法封裝爲一個裝飾器, 注入到每一個頁面組件中, 那麼每一個頁面組件就能夠直接使用這個 snackbar(操做提示框) 了.
function withSnackbar (ComposedComponent) { return class withSnackbar extends Component { // ... render() { return ( <div> <ComposedComponent {...this.props} /> <Snackbar {...this.state}/> </div> ); } }; }
import withStyles from '../../decorators/withStyles'; import withViewport from '../../decorators/withViewport'; import withSnackbar from '../../decorators/withSnackbar'; // 裝飾器 @withViewport @withStyles(styles) @withSnackbar class Page extends Component { // ... }
匿名函數使用箭頭函數能夠這麼寫:
const emptyFunction = () = > { /*do nothing*/ };
有了箭頭函數, 媽媽不再怕 this 突變了...
const socket = io('http://localhost:9999'); socket.on('connect', () => { socket.on('component', (data) => { // 這裏的 this 不會突變到指向 window this.showSnackbar('本地組件已更新, 自動刷新'); this.getComponent(data.project, data.component); } }); });
使用 fetch 加 await 替代 XHR.
fetch 比起 xhr, 作的事情是同樣的, 只是接口更加語義化, 且支持 Promise.
配合 async/await 使用的話, 那叫一個酸爽!
try { const res = await fetch(`/api/generate/`, { method: 'post', // 指定請求頭 headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, // 指定請求體 body: JSON.stringify(data) }); // 返回的是一個 promise, 使用 await 去等待異步結果 const json = await res.json(); if (json.retcode === 0) { this.showSnackbar('生成成功'); } else { this.showSnackbar('生成失敗'); } } catch (error) { console.error(error); this.showSnackbar('生成失敗'); }
本地開發一個組件時, 監聽文件變化, 使用 WebSocket 通知頁面更新.
起一個 socket 服務, 監聽文件變化:
async function watchResources() { var io = require('socket.io')(9999); io.on('connection', function (socket) { event.on('component', (component) => { socket.emit('component', component); }); }); console.log('watching: ', path.join(__dirname, '../src/resources/**/*')); watch(path.join(__dirname, '../src/resources/**/*')).then(watcher => { watcher.on('changed', (filePath) => { console.log('file changed: ', filePath); // [\/\\] 是爲了兼容 windows 下路徑分隔的反斜槓 const re = /resources[\/\\](.*)[\/\\]components[\/\\](.*)[\/\\](.*)/; const results = filePath.match(re); if (results && results[1] && results[2]) { event.emit('component', { project: results[1], component: results[2] }); } }); }); }
預覽組件的頁面監聽文件變化, 變化後從新向服務器拉取最新編譯好的組件, 進行更新.
componentDidMount = () => { const socket = io('http://localhost:9999'); socket.on('connect', () => { socket.on('component', (data) => { if ((data.project === this.state.component.project) && (data.component === this.state.component.name)) { console.log('component changed: ', data.project, data.component); this.showSnackbar('本地組件已更新, 自動刷新'); // 從新向服務器拉取最新編譯好的組件, 進行更新 this.getComponent(data.project, data.component); } }); }); }
生成頁面時須要預覽頁面, 爲了不頁面樣式被系統樣式影響, 應該使用內嵌 iframe 的方式來隔離樣式.
父頁面使用 postMessage 與子頁面進行通訊:
const postPageMessage = (page) => { document.getElementById('pagePreviewIframe').contentWindow.postMessage({ type: 'page', page: page }, '*'); }
子頁面監聽父頁面數據變化, 更新頁面:
window.addEventListener("message", (event) => { // if(event.origin !== 'http://localhost:3000') return; console.log('previewPage receives message', event); if (event.data.type === 'page') { this.setState({ page: event.data.page }); } }, false);
本文是項目設計介紹, 歡迎你們多多指正. 等我把鑑權功能和版本管理加上,就能夠用於生產環境啦, 敬請期待.