前言:以前寫了2篇基本沒有乾貨,怕讀者罵我狗,既然分享了就完全點,都是幹代碼的,有啥不能寫的。具體的代碼我會發到github進行開源。整個項目就不開源了,只開源編輯部分。代碼是react代碼,沒辦法一直就用這個框架,意思到了不要太看中語法框架。沒有時間對龐大的代碼進行一一講解,各位且本身看吧,嫌煩就別看了。看看這裏的思路就行了。css
咱們仍是從架構講吧,不少人問前端架構幹啥的,其實我也不知道,我通過winter(臥槽,winter是誰,不知道的請自行搜索)指點,告訴我前端架構就是解決複用性的問題,豁然開朗,請不要再給前端架構搞的神祕而遙不可及。以React爲例,就是合理的分拆組件,到達最高的複用和擴展。隨着項目不一樣也不盡相同,固然不少人喜歡用*-cli等工具生產項目,這個不糾結,反正我以爲靈活度不夠,一直都是本身作。html
通常個人項目裏面README.md開頭都會有這麼一個圖,說明當前項目的文檔目錄結構,以便參與項目的同事看得懂代碼。前言廢話說完,進入正題,項目目錄不是重點。本人由於是react系開發者,因此代碼以react爲主,可是我分享的基本原則通用,未必非react不可。前端
根據上一篇的產品形態,我們針對於2類產品分析,一類就是H5頁面生成器,一類就是流程定義+form表單生成。node
先說H5頁面編輯器,來重複一下基本的產品需求react
基於上述的問題,咱們須要拆解需求,造成一個基礎的前端框框,把這些需求點框進去,無論UI,UI是很簡單的,沒有任何須要架構的地方,根據prd去畫就行了。基本編輯的UI也就是左中右結構,左側組件列表,中間效果展現,右側屬性配置。基本如是jquery
咱們用react代碼實現以下, 入口 editor.html, editor.jsgit
<div className="page-container">
<Suspense fallback="">
<ThemeProvider theme={theme}>
<Header savePage={this.savePage} publishPage={this.publishPage} />
<Operation compCount={compCount} />
<Preview pageConf={pageConf} compList={compList} />
<Config
pageConf={pageConf}
compList={compList}
currentUuid={currentUuid}
onSort={this.handleSort}
/>
</ThemeProvider>
</Suspense>
</div>
複製代碼
這是UI的設計,外層的 <div/> <Suspense/> <ThemeProvider/>忽略github
<Header/> - 頭部包含返回按鈕,預覽,保存,發佈等按鈕功能ajax
<Operation/> - 左側的組件列表markdown
<Preview/> - 中間的預覽展現,爲了很好的隔絕dom結構和event事件,中間使用iframe加載一個preview.html,利用postmate組件進行iframe通訊工做。其中暴露不少問題,後面會詳細講解。
<Config/> - 就是根據用戶選中的組件,展現相應的配置項
這裏講下整個項目的通訊閉環,我想你們都很容易明白技術實現的過程當中是怎麼作的了。整個頁面UI其實都是一份數據結構在驅動。利用react的內部數據雙向綁定,每一次的新建修改其實都是在改同一份數據結構。舉個列子:
env: "publish",
pageId: "1632599993707810817",
pageData: {
pageConf:{
background: {a: 1, r: 255, b: 255, g: 255}
pageDec: ""
pageName: "杭州印象"
pageURL: "https://cdn.izelas.run/publish/2194972e.html"
},
compList: [
{
uuid: "c3f1ecdc71c1df833a4498fddd4040db",
compLabel: "輪播",
compName: "Carousel",
chosen: false,
setting: {
carouselProps: {effect: "scrollx", autoplay: true},
imageList: (5) [{…}, {…}, {…}, {…}, {…}],
outLine: false,
place: "inline",
}
}
]
},
複製代碼
秀到這你們就明白了,其實技術總體沒什麼神祕的,也符合大多數研發者的實現。上述配置就是一個頁面裏面包含了一個輪播組件,後的會在用戶點擊發布的時候,把數據和渲染作一次加載,打包成靜態文件,推送到cdn上,返回一個靜態頁面地址,這就是整個邏輯。由於後臺是nodejs那塊代碼相對簡單,回頭再放,主要是前端的渲染。下面你們要作幾件事
看UI代碼,你們會發現,入口頁面持有全部的數據,任何子組件不持有數據,經過組件props屬性進行傳遞。而事件經過廣播或者監聽模式完成。對於這個模式不清楚的,我稍微解釋一下,由於react在父子組件的事件上能夠經過props注入完成,若是是兄弟組件,或者更遠的平級組件,就須要藉助共同的父組件來傳遞事件。效率不高,還容易搞混。如今的模式是,當我點擊一個按鈕,這個按鈕其實沒有實際邏輯操做,只是大喊一聲我被點了,就完了,把本身被點了這個事廣播出去,那麼誰處理?誰監聽誰處理。就好比老師上課點名,張三,這個時候全班同窗都聽到了,可是隻有張三答了「到」。就是這個意思。
// 持有 pageData -> compList,pageConf 的頁面監聽事件
// 支持用戶點擊新建,這裏點擊觸發是在operation組件,向此處廣播事件
emitter.addListener('_micro_page_createComp_', this.createComp)
// 監聽數據變化,而後通知底層組件 包括組件建立,刪除,複製,位置調整
emitter.addListener('_micro_page_onSetCompList_', this.onSetCompList)
// 監聽組件屬性值改變的 設置某個組件屬性
emitter.addListener('_micro_page_setCompProps_', this.setCompData)
// 監聽頁面設置
emitter.addListener('_micro_page_setPageConfig_', this.setPageConfig)
// 監聽歷史記錄回滾消息
emitter.addListener('_micro_page_posiCompList_', this.goBackAction)
// 組件列表操做消息監聽
emitter.addListener('_micro_page_compListeAction_', this.buildCompListData)
複製代碼
// 按鈕的實現就很是簡單了,只要把UI畫上,點擊一下就把被點擊的事件廣播出去,附帶一個默認配置參數
// 剩下的就交給監聽了此時間的方法,這個頂層數據發生變化,那麼react會自動diff,全部接收到props
// 的組件都會更新props,而後更新UI。config的原理是同樣的。這樣就把數據和處理的方法所有集中在
// 入口文件中,邏輯比較清晰,容易排查問題。
<div
className="module-item"
onClick={() => {
emitter.emit('_micro_page_createComp_', _cloneDeep(defaultConfig))
}}
>
<div className="module-item-inner">
<SvgIcon component={renderComponent(compName)} viewBox="0 0 22 22"/>
<div className="module-item-name">{compLabel}</div>
</div>
</div>
複製代碼
這個邏輯講明白了,其實就不復雜了,無非就是組件多一點,UI界面複雜一些而已。根據prd的組件設計去開發就好,沒什麼難度。還剩一些問題,就是數據埋點和ajax數據通訊。
數據埋點通常你們都會寫一個比較通用的上報函數,也比較簡單。很是成熟,不會github上搜搜太多。不行就看看GA(google analysis)的代碼。主要是一些按鈕數據的上報,好比點擊某個商品,雖然商品頁面會統計商品的pv,可是不知道是從營銷頁面來的。相似的問題不少,固然GA也提供了方法能夠在點擊事件發生時候主動上報數據,問題來了,自己低代碼的平臺,頁面尚未發佈,須要根據用戶製做頁面的結果生成相應的頁面,可能咱們都不知道用戶須要選什麼商品。還有一個問題就是代碼侵入,若是GA的方法改了呢,那麼我就要改。得不償失,個人解法就是便籤隱藏屬性。類型jquery的data-,而後根據數據埋點的須要把須要的數據放到一個自定義屬性中,數據收集組件本身監聽pop上來的事件,到dom中自行取用。
還有ajax請求的問題,這個問題用前端的辦法確實沒啥好解的,不是不能作,只是用nodejs的解法更方便,反正我也是要根據配置,把頁面打成靜態的文件,上傳到cdn的,那麼幹脆就在這一步作了就行了。其實沒啥難度,就是掃描一下全部組件,發現有ajax請求的,nodejs發起請求,把請求回來的數據靜態化到配置項裏面,而後調用cdn方法把html文件上傳就行了。
const _putHTMLToOss = async (pageConf, compList, pageId) => {
try {
// 獲取全部動態數據
const _compList = await getAllData(compList)
// 設置靜態頁面url
const publishHtmlPath = pageConf.pageURL
? pageConf.pageURL.replace("https://cdn.izelas.run/", "")
: `hd/publish/${getShortStr()}.html`;
if (!pageConf.pageURL)
pageConf.pageURL = `https://m.myweimai.com/${publishHtmlPath}`;
// 裝配模板須要的數據
const injector = {
jses: [
`${cdnPath}/micropage/${timestamp.micropage}/common.js`,
`${cdnPath}/micropage/${timestamp.micropage}/micro-view.js`,
],
css: [
venders.materialUI,
`${cdnPath}/micropage/${timestamp.micropage}/common.css`,
`${cdnPath}/micropage/${timestamp.micropage}/micro-view.css`,
],
data: JSON.stringify({
env,
pageId,
pageData: {
pageConf,
compList: _compList,
},
weimaiHosts,
}),
title: (pageConf && pageConf.pageName) || "微脈-模塊化編輯器",
};
// 讀取模板文件
const renderString = fs.readFileSync(path.join(view_path, "view.html"), {
encoding: "utf-8",
});
// 編譯模板
const compiled = _.template(renderString);
// 數據和模板結合成已編譯好的字符串
const compiledStr = compiled(injector);
// 調用oss受權客戶端
const ossClient = new OSS(ossKey);
// 上傳文件到oss
await ossClient.put(publishHtmlPath, Buffer.from(compiledStr));
return true;
} catch (error) {
console.error(error);
return false;
}
};
複製代碼
此文代碼基本是截取,會意不要生搬,畢竟每一個系統都有特殊性,尤爲是行業對於產品的要求,這套代碼的源碼會開源到github,等我整理完,由於nodejs代碼有驗證和一些技術點不方便開源,只放前端代碼。仍是那個意思,明白思路,根據本身的業務本身思考,能幫到各位最好,有好的解決方案歡迎提供。系統也在不斷的升級中。還須要加入一些js和圖片資源加載的優化,還有就是模板和換膚的一些思考。這些有待各位同仁的共同努力。