厭倦了寫活動頁?快來擼一個頁面生成器吧!

前言

若是你常常接觸一些公司的活動頁,可能會常常頭疼如下問題:這些項目週期短,需求頻繁,迭代快,技術要求不高,成長空間也小。可是咱們仍是快馬加鞭的趕着產品提來的一個個需求,隨着公司規模的增長,咱們不可能無限制的增長人手不斷地重複着這些活動。這裏我就不具體介紹一些有的沒的的一些概念了,由於要介紹的概念實在太多了,做爲一個前端的咱們,直接上代碼擼就行了!!!! 想要了解更多,也歡迎訪問:javascript

blogshtml

目標

咱們的目標是實現一個頁面製做後臺,在後臺中咱們能夠對頁面進行 組件選擇 --> 佈局樣式調整 --> 發佈上線 --> 編輯修改這樣的流程操做。前端

架構設計

首先是要能提供組件給用戶進行選擇,那麼咱們須要一個組件庫,而後須要對選擇的組件進行佈局樣式調整,因此咱們須要一個頁面編輯後臺接着咱們須要將編輯產出的數據渲染成真實的頁面,因此咱們須要一個node服務和用於填充的template 模板。發佈上線,這個直接對接各個公司內部的發佈系統就行了,這裏咱們不作過多闡述。最後的編輯修改功能也就是針對配置的修改,因此咱們須要一個數據庫,這裏我選擇的是用了mysql。固然你也能夠順便作作權限管理,頁面管理....等等之類的活。 囉嗦了這麼長,咱們來畫個圖,瞭解下大概的流程:vue

開擼

組件的實現

首先咱們來實現組件這一部分,由於組件關聯着後臺編輯的預覽和最後發佈的使用。組件設計咱們應該儘可能保持組件的對外一致性,這樣在進行渲染的時候,咱們能夠提供一個統一的對外數據接口。這裏咱們的技術選型是基於 Vue 的,因此下面的代碼部分也主要是基於 Vue 的,可是萬變不離其宗,其餘語言也相似。java

根據上圖,咱們的組件是會被一個個拆分單獨發佈到 npm倉庫的,爲何這麼設計呢?其實以前也考慮過設計成一個組件庫,全部組件都包含在一個組件庫內,這樣只須要發佈一個組件庫包,用的時候按需加載就行了。後來在實踐的過程當中發現這樣並不合適協同開發,其餘前端若是想貢獻組件,接入的改形成本也很大。舉個🌰:小明在業務中寫了個Button組件,這個組件常常會被其餘項目複用,他想把這個組件貢獻到咱們的系統中,被模板使用,若是是一個組件庫的話,他首先得拉取咱們組件庫的代碼,而後按照組件庫的規範格式進行提交。這樣一來,偷懶的小明可能就不太願意這麼幹,最爽的方法固然是在本地構建一個npm庫,開發選用的是用TypeScript仍是其餘的咱們不關心,選用的 Css 預處理器咱們也不關心,甚至編碼規範的ESLint咱們也不關心。最後只需經過編譯後的文件便可。這樣就避免了一個組件庫的約束。依託於NPM完善的發佈/拉取,以及版本控制機制,可讓咱們少作一些額外的工做,也能夠快速的把平臺搭建起來。node

說了這麼多,代碼呢?,咱們以一個Button爲例,咱們對外提供這樣的形式組件:mysql

<template>
  <div :style="data.style.container" class="w_button_container">
    <button :style="data.style.btn"> {{data.context}}</button>
  </div>
</template>
<script> export default { name: 'WButton', props: { data: { type: Object, default: () => {} } } } </script>
複製代碼

能夠看到咱們只對外暴露了一個props,這樣作法的好處是能夠統一組件對外暴露的數據,組件內部愛怎麼玩怎麼玩。注意,這裏咱們也能夠引入一些第三方組件庫,好比mint-ui之類的。webpack

後臺編輯的實現

在寫代碼前,咱們先考慮一下須要實現哪些功能:git

  1. 一個屬性編輯區,提供給使用者編輯組件內部props的功能
  2. 一個組件選擇區,提供使用者選擇須要的組件
  3. 一個組件預覽區,提供使用者拖拽排序頁面預覽的功能
編輯區的實現

按照順序,咱們先來實現組件的屬性編輯功能。咱們要考慮,一個組件暴露出哪些可配置的信息。這些可配置的信息如何同步到後臺編輯區,讓使用者進行編輯,一個按鈕的可配置信息多是這樣:github

image

若是把這些配置所有寫在後臺庫裏面,根據當前選擇的組件加載不一樣的配置,維護起來會至關麻煩,並且隨着組件數量的增長,也會變得臃腫,因此咱們能夠將這些配置存儲在服務端,後臺只須要根據存儲的規則進行解析即可,舉個例子,咱們其實能夠存儲這樣的編輯配置:

[
  {
    "blockName": "按鈕佈局設置", 
    "settings": {
      "src": {
        "type": "input",  
        "require": true,
        "label": "按鈕文案"
      }
    }
  }
]
複製代碼

咱們在編輯後臺,經過接口請求到這些配置,即可以進行規則渲染:

/** * 根據類型,選擇建立對應的組件 * @param {VNode} vm * @returns {any} */
    createEditorElement (vm: VNode) {
      let dom = null
      switch (vm.config.type) {
        case 'align':
          dom = this.createAlignElement(vm)
          break;
        case 'select':
          dom = this.createSelectElement(vm)
          break;
        case 'actions':
          dom = this.createActionElement(vm)
          break;
        case 'vue-editor':
          dom = this.createVueEditor(vm)
          break;
        default:
          dom = this.createBasicElement(vm)
      }
      return dom
    }
複製代碼
組件選擇區

首先咱們須要考慮的是,組件怎麼進行註冊?由於組件被用戶選用的時候,咱們是須要渲染該組件的,因此咱們能夠提供一段 node 腳原本遍歷所需組件,進行組件的安裝註冊:

// 定義渲染模板和路徑
var OUTPUT_PATH = path.join(__dirname, '../packages/index.js');
console.log(chalk.yellow('正在生成包引用文件...'))
var INSTALL_COMPONENT_TEMPLATE = ' {{name}}';
var IMPORT_TEMPLATE = 'import {{componentName}} from \'{{name}}\'';
var MAIN_TEMPLATE = `/* Automatic generated by './compiler/build-entry.js' */ {{include}} const components = [ {{install}} ] const install = function(Vue) { components.map((component) => { Vue.component(component.name, component) }) } /* istanbul ignore if */ if (typeof window !== 'undefined' && window.Vue) { install(window.Vue) } export { install, {{list}} } `;
// 渲染引用文件
var template = render(MAIN_TEMPLATE, {
  include: includeComponentTemplate.join(endOfLine),
  install: installTemplate.join(`,${endOfLine}`),
  version: process.env.VERSION || require('../package.json').version,
  list: listTemplate.join(`,${endOfLine}`)
});

// 寫入引用
fs.writeFileSync(OUTPUT_PATH, template);
複製代碼

最後渲染出來的文件大概是這樣:

import WButton from 'w-button'
const components = [
    WButton
]
const install = function(Vue) {
    components.map((component) => {
        Vue.component(component.name, component)
    })
}
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
    install(window.Vue)
}
export {
    install,
    WButton
}
複製代碼

這個也是組件庫的通用寫法,因此這裏的思想就是把發佈到npm上的組件,進行聚合,聚合成一個組件包引用,咱們在後臺編輯的時候,是須要全量引入的:

import * as W_UI from '../../packages'

Vue.use(W_UI)
複製代碼

這樣,咱們組件便註冊完了,組件選擇區,主要是提供組件的可選項,咱們能夠遍歷組件,提供一個個 List 讓用戶選擇,固然若是咱們每一個組件若是隻提供一個組件名,用戶可能並不知道組件長什麼樣,因此咱們最好能夠提供一下組件長什麼樣的縮略圖。這裏咱們能夠在組件發佈的時候,也經過 node 腳本進行。這裏要實現的代碼比較多,我就大體說一下過程,由於也不是核心邏輯,無關緊要,只能說有了體驗上會好一點:

  1. 用戶啓用 dev-server 進行代碼編寫測試
  2. server 腳本使用 Chrome 工具 puppeteer,調整頁面到手機端模式, 進行當前 dev-server 截圖。
  3. 生成截圖文件,上傳到node服務,關聯組件

這樣,就能夠在加載組件選擇區的時候,爲組件附上縮略圖。

組件預覽區

當用戶在選擇區選擇了組件,咱們須要展現在預覽區域,那麼咱們怎麼知道用戶選擇了哪些組件呢?總不能提早所有把組件寫入渲染區域,經過 v-if來判斷選擇吧?固然沒有這麼蠢,Vue 已經提供了動態組件的功能了:

<div :class="[index===currentEditor ? 'active' : '']" :is="select.name" :data="select.data">
</div>
複製代碼

爲何咱們不用縮略圖代替真實組件?一方面生成的縮略圖尺寸存在問題,另外一方面,咱們須要編輯的聯動性,就是編輯區的編輯須要及時的反饋給用戶。

額外的問題

說了這麼多,貌似一切都很順利,可是這樣在實踐的時候,發現了存在一個明顯的問題就是:咱們中間的預覽區域其實就是爲了儘量模擬移動端頁面效果。可是若是咱們加入了一些包含相似 position: fixed 樣式的組件,會發現樣式上就出現了明顯的問題。典型的好比Dialog Loading 等。 因此咱們參考了 m-ui組件庫的設計,將中間預覽操做容器展現爲一個iframe。將iframe大小調整爲375 * 667,模擬 iPhone 6 的手機端。這樣就不會存在樣式問題了。但是這樣又出現了另外一個難點,那就是左側的編輯數據如何及時的反應到iframe中?沒錯,就是postMessgae,大體思路以下:

利用 vuex 作數據存儲池,全部的變化,經過 postMessgae進行同步,這樣咱們只用確保數據池中的數據變化,即可以映射到渲染層的變化。好比,咱們在預覽區進行了組件選擇和拖拽排序,那麼咱們只需經過vuex出發同步信息即可:

// action.ts
const action = {
  setCurrentPage ({commit, state}, page: number) {
      // 更新當前store
      commit('setCurrentPage',page)
      // 對應postMessage
      helper.postMsgToChild({type: 'syncState', value: state})
    },
  // ...
}
複製代碼

Template 模板的實現

模板的設計實現,我參考了 Vue-cli 2.x 版本的思想,把這裏的模板,存在了對應的 git 倉庫中。當用戶須要進行頁面構建的時候,直接從 git 倉庫中拉取對應的模板便可。固然拉取完,也會緩存一份在本地,之後渲染,直接從本地緩存中讀取便可。咱們如今把中心放在模板的格式和規範上。模板咱們採用什麼樣的語法無所謂,這裏我才用了和 Vue-cli同樣的Handlerbars引擎。這裏直接上咱們模板的設計:

<template>
  <div class="pg-index" :style="{backgroundColor: '{{bgColor}}'}">
      <div class="main-container" :style="{ backgroundColor: '{{bgColor}}', backgroundImage: '{{bgImage}}' ? 'url({{bgImage}})' : null, backgroundSize: '{{bgSize}}', backgroundRepeat: 'no-repeat' }">
        {{#components}}
          <div class="cp-module-editor {{className}} {{data.className}}">
            <{{name}} class="temp-component" :data="{{tostring data}}" data-type="{{upcasefirst name}}"></{{name}}>
          </div>
        {{/components}}
      </div>
  </div>
</template>

<script> {{#noRepeatCpsName}} import {{upcasefirst this}} from '{{this}}' {{/noRepeatCpsName}} export default { name: '{{upcasefirst repoName}}', components: { {{#noRepeatCpsName}} {{upcasefirst this}}, {{/noRepeatCpsName}} } } </script>

複製代碼

爲了簡化邏輯,咱們把模板都設計成流式佈局,全部組件一個個堆疊往下順序排列。這個文件即是咱們vue-webpack-simple的模板中的App.vue。咱們對其進行了改寫。這樣在數據填充萬,即可以渲染出一個 Vue 單文件。這裏我只舉着一個例子,咱們還能夠實現多頁模板等等複雜的模板,根據需求拉取不一樣的模板便可。

Node 渲染服務

當後臺提交渲染請求的時候,咱們的 node 服務所作的工做主要是:

  1. 拉取對應模板
  2. 渲染數據
  3. 編譯

拉取也就是去指定倉庫中經過download-git-repo插件進行拉取模板。編譯其實也就是經過metalsmith靜態模板生成器把模板做爲輸入,數據做爲填充,按照handlebars的語法進行規則渲染。最後產出build構建好的目錄。在這一步,咱們以前所需的組件,會被渲染進package.json文件。咱們來看一下核心代碼:

// 這裏就像一個管道,以數據入口爲生成源,經過renderTemplateFiles編譯產出到目標目錄
function build(data, temp_dest, source, dest, cb) {
  let metalsmith = Metalsmith(temp_dest)
    .use(renderTemplateFiles(data))
    .source(source)
    .destination(dest)
    .clean(false)

  return metalsmith.build((error, files) => {
    if (error) console.log(error);
    let f = Object.keys(files)
      .filter(o => fs.existsSync(path.join(dest, o)))
      .map(o => path.join(dest, o))
    cb(error, f)
  })
}


function renderTemplateFiles(data) {
  return function (files) {
    Object.keys(files).forEach((fileName) => {
      let file = files[fileName]
      // 渲染方法
      file.contents = Handlebars.compile(file.contents.toString())(data)
    })
  }
}
複製代碼

最後咱們獲得的是一個 Vue 項目,此時還不能直接跑在瀏覽器端,這裏就涉及到當前發佈系統所支持的形式了。怎麼說?若是你的公司發佈系統須要在線編譯,那麼你能夠把源文件直接上傳到git倉庫,觸發倉庫的 WebHook 讓發佈系統替你發掉這個項目便可。若是大家的發佈系統是須要你編譯後提交編譯文件進行發佈的,那麼你能夠經過 node 命令,進行本地構建,產出 HTML,CSS,JS。直接提交給發佈系統便可。 到這裏,咱們的任務就差很少了~具體的核心實心大多已經闡述清楚,若是實現當中有什麼問題和不妥,也歡迎一塊兒探討交流!!

題外話

實現這樣一套頁面構建系統,其實我這裏簡化了不少東西,旨在給你們提供一種思路。另外,其實咱們的頁面所有在服務端構建的時候產出,咱們能夠再服務端這一層作不少工做,好比頁面的性能優化,由於頁面數據咱們所有都有,咱們也能夠作頁面的預渲染,骨架屏,ssr,編譯時優化等等。並且咱們也能夠對產出的活動頁作數據分析~有不少想象的空間。

相關文章
相關標籤/搜索