雪球系統,經過拖拽基礎組件,組合成複合組件的一個桌面應用,最終以.vue文件的形式保存到指定目錄,旨在加快工做效率。javascript
操做系統爲 Mac OS。css
從gif中能夠看出應用的主要功能,界面左側爲組件列表,中間部分是效果展現區和代碼展現區,界面右側爲操做按鈕。從操做中能夠看出,隨着基礎組件被拖拽到效果展現區,不只效果變了,代碼展現區的代碼也跟着變了,最終以vue文件的形式產出。 html
snowBallvue
第一次在掘金髮文,寫的很差請多擔待,項目僅供學習,請勿商用,如轉載請標明出處。最後,以爲不錯的小夥伴給一下star哦~java
展現中有明顯的IO操做,咱們來作個對比, 傳統的web項目:若是要實現IO操做,要基於node來作,部署到服務器上,涉及到修改源文件到話,安全性欠佳。 electron:Electron經過將Chromium和Node.js合併到同一個運行時環境中,並將web項目打包爲Mac,Windows和Linux系統下的應用。 實際項目中會修改到工程中到源文件,因此最終選擇electron做爲最終方案。node
Electron 運行 package.json 的 main 腳本的進程被稱爲主進程。 在主進程中運行的腳本經過建立web頁面來展現用戶界面。 一個 Electron 應用老是有且只有一個主進程。linux
因爲 Electron 使用了 Chromium 來展現 web 頁面,因此 Chromium 的多進程架構也被使用到。 每一個 Electron 中的 web 頁面運行在它本身的渲染進程中。webpack
在普通的瀏覽器中,web頁面一般在沙盒環境中運行,而且沒法訪問操做系統的原生資源。 然而 Electron 的用戶在 Node.js 的 API 支持下能夠在頁面中和操做系統進行一些底層交互。git
主進程使用 BrowserWindow 實例建立頁面。 每一個 BrowserWindow 實例都在本身的渲染進程裏運行頁面。 當一個 BrowserWindow 實例被銷燬後,相應的渲染進程也會被終止。github
主進程管理全部的web頁面和它們對應的渲染進程。 每一個渲染進程都是獨立的,它只關心它所運行的 web 頁面。
在頁面中調用與 GUI 相關的原生 API 是不被容許的,由於在 web 頁面裏操做原生的 GUI 資源是很是危險的,並且容易形成資源泄露。 若是你想在 web 頁面裏使用 GUI 操做,其對應的渲染進程必須與主進程進行通信,請求主進程進行相關的 GUI 操做。
Electron爲主進程( main process)和渲染器進程(renderer processes)通訊提供了多種實現方式,如可使用ipcRenderer 和 ipcMain模塊發送消息,使用 remote模塊進行RPC方式通訊。
項目基於 electron-vue
# 安裝 vue-cli 和 腳手架樣板代碼
npm install -g vue-cli
vue init simulatedgreg/electron-vue my-project
# 安裝依賴並運行你的程序
cd my-project
yarn # 或者 npm install
yarn run dev # 或者 npm run dev
複製代碼
運行起來以後,會自動起一個客戶端。
先考慮單個物體碰撞,以下圖所示,拖拽物體D若是一直在容器Container的左側、右側、上側或者下側的話,那就表明一直沒有碰撞。同理,當檢測是否與多個物體碰撞時,只須要將多個物體的參數與拖拽物體D的參數作對比就能夠了。
mousedown(e, tag) {
// oDiv爲拖拽物體
let oDiv = this.$refs.move
this.nowTag = tag
let left = getElementPageLeft(e.target)
let top = getElementPageTop(e.target)
oDiv.style.left = left + 'px'
oDiv.style.top = top + 'px'
this.showMove = true
this.disX = e.clientX - oDiv.offsetLeft - left
this.disY = e.clientY - oDiv.offsetTop - top
// oDiv2爲總容器
let oDiv2 = this.$refs.hideBox
let children = Array.from(oDiv2.childNodes)
this.mousemove(oDiv, children.length > 0 ? children : [oDiv2], this.disX, this.disY, children.length > 0)
},
mousemove(oDiv, arr, disX, disY, needTest = false) {
// needTest表示總容器中有子容器,若檢測完子容器沒有碰撞,則須要再檢測與總容器是否碰撞
// 將須要使用到到目標容器屬性存在數組中,減小dom操做
arr = arr.map((item, index) => {
const obj = {
className: item.className,
t2: getElementPageTop(item),
l2: getElementPageLeft(item),
r2: getElementPageLeft(item) + item.offsetWidth,
b2: getElementPageTop(item) + item.offsetHeight
}
return obj
})
// 移動物體跟着鼠標移動
document.onmousemove = ev => {
oDiv.style.left = ev.clientX - disX + 'px'
oDiv.style.top = ev.clientY - disY + 'px'
}
// 鼠標擡起檢測碰撞
document.onmouseup = (ev) => {
ev = ev || window.event
let t1 = getElementPageTop(oDiv)
let l1 = getElementPageLeft(oDiv)
let r1 = getElementPageLeft(oDiv) + oDiv.offsetWidth
let b1 = getElementPageTop(oDiv) + oDiv.offsetHeight
for (let index = 0; index < arr.length; index++) {
let item = arr[index]
// isKnocked爲true表示沒有碰撞
const isKnocked = b1 < item.t2 || l1 > item.r2 || t1 > item.b2 || r1 < item.l2
arr[index].isKnocked = !isKnocked
}
// 狀態重置
document.onmousemove = null
document.onmouseup = null
oDiv.style.left = '0px'
oDiv.style.top = '0px'
this.showMove = false
// 篩選結果 當檢測完沒有碰到到子盒子時,繼續檢測是否碰撞到父盒子
let result = arr.filter((item, index) => {
return item.isKnocked
})
if (result.length > 0) {
this.knockSuccess(result[0].className)
} else {
if (needTest) {
let oDiv2 = this.$refs.hideBox
const object = {
className: oDiv2.className,
t2: getElementPageTop(oDiv2),
l2: getElementPageLeft(oDiv2),
r2: getElementPageLeft(oDiv2) + oDiv2.offsetWidth,
b2: getElementPageTop(oDiv2) + oDiv2.offsetHeight
}
// isKnocked爲true表示沒有碰撞
const isKnocked = b1 < object.t2 || l1 > object.r2 || t1 > object.b2 || r1 < object.l2
if (!isKnocked) {
this.knockSuccess(oDiv2.className)
}
console.log('object~~~~', object, isKnocked)
}
}
console.log('~~~~~碰撞結束', result[0])
}
}
複製代碼
其中getElementPageLeft和getElementPageTop爲物體離可視區左側和上側的真實距離
const getElementPageLeft = (element) => {
var actualLeft = element.offsetLeft
var parent = element.offsetParent
while (parent != null) {
actualLeft += parent.offsetLeft + (parent.offsetWidth - parent.clientWidth) / 2
parent = parent.offsetParent
}
return actualLeft
}
const getElementPageTop = (element) => {
var actualTop = element.offsetTop
var parent = element.offsetParent
while (parent != null) {
actualTop += parent.offsetTop + (parent.offsetHeight - parent.clientHeight) / 2
parent = parent.offsetParent
}
return actualTop
}
複製代碼
碰撞檢測以後要作的事情就是SFC的拆分與組合。 vue的SFC文件都有必定的格式
SFC拆分紅3部分
import fs from 'fs'
import path from 'path'
// 獲取vue文件並拆分
const getVueContent = (src) => {
let fileContents = fs.readFileSync(path.join(__static, src), 'utf8')
return splitTmp(fileContents)
}
// vue字符串拆分紅template、js、css
const splitTmp = (fileContents) => {
const scriptReg = /<script.*?>([\s\S]+?)<\/script>/img
const temReg = /<template>([\s\S]+?)<\/template>/img
const styleReg = /<style.*?>([\s\S]+?)<\/style>/img
let scriptResult = scriptReg.exec(fileContents)[1]
let temResult = temReg.exec(fileContents)[1]
let styleResult = styleReg.exec(fileContents)[1]
scriptResult = scriptResult.split('export default')[1].replace(/(^\s*)|(\s*$)/g, '')
return {
temResult,
scriptResult,
styleResult
}
}
複製代碼
函數不能經過JSON.stringify()的方式直接轉成字符串,因此經過fnToString方法處理,並將props的特殊狀況一同處理
// 轉js模塊成帶有###Fn### 的字符串
const JsonToSpecialStr = (data) => {
data = fnToString(data)
data = JSON.stringify(data)
if (data.indexOf('\\n')) {
data = data.replace(/\\n/g, ' \n').replace(/["]/g, '') } return data } // 將json對象中的方法轉換成字符串,並在方法前加上###Fn### 方便後面去除 const fnToString = (data) => { for (let [key, value] of Object.entries(data)) { if (typeof value === 'string') { data[key] = value } else if (typeof value === 'function') { data[key] = `###Fn###${value.toString()}` } else if (key === 'props') { let props = '' for (let [key1, value1] of Object.entries(value)) { let name = value1.type.name if (name === 'String') { props += `${key1}:{default: '${value1.default}', type: ${name}},` } else if (name === 'Number' || name === 'Boolean') { props += `${key1}:{default: ${value1.default}, type: ${name}},` } else if (name === 'Object' || name === 'Array') { props += `${key1}:{default: () => {}, type: ${name}},` } data[key] = `{${props.substr(0, props.length - 1)}}` } } else { fnToString(data[key]) } } return data } 複製代碼
後續在合併的時候去除###Fn###
// 去除###Fn###
const FnMove = (string) => {
const FnMoveReg = /###Fn###([\s\S]+?)\(/img
let moveStr = FnMoveReg.exec(string)
if (moveStr === null) {
return string
}
string = string.replace(`${moveStr[1]}:###Fn###`, '')
return FnMove(string)
}
複製代碼
代碼組合好以後,格式不是很工整,因此須要美化一下。 安裝依賴vue-beautify
npm i vue-beautify -S
複製代碼
使用以下,其中options爲格式配置項
var vueBeautify = require('vue-beautify')
// 美化模版
const beautifyTmp = (res) => {
const options = {
intent_scripts: 'keep',
indent_size: 2,
space_in_empty_paren: true
}
let output = `
<template>${res.temResult}</template>
<script>
export default ${res.scriptResult}
</script>
<style scoped lang="less">
${res.styleResult}
</style>
`
output = FnMove(output)
output = vueBeautify(output, options)
return output
}
複製代碼
效果展現區實現熱更新過程:eletron項目啓動的同時會啓動vue-tmp,並開啓熱更新,主項目中用iframe來展現vue-tmp頁面。當基礎組件被拖拽碰撞到容器時,將基礎組件寫入vue-tmp項目中的App.vue中,webpack-dev-server檢測到變更,刷新視圖。
這裏要實現eletron啓動以後自執行命令行語句,因此要用到node的子進程child_process。
啓動vue-tmp前要檢查一下端口是否被佔用,若是佔用要先釋放端口(linux和windows方式不同,這裏只實現了linux)
const exec = require('child_process').exec
// 任何你指望執行的cmd命令,ls均可以
let findStr = 'lsof -i:1112'
// 執行cmd命令的目錄,若是使用cd xx && 上面的命令,這種將會沒法正常退出子進程
let cmdPath = './'
// 子進程名稱
let findProcess, killProcess
function runFindExec(Fn) {
// 執行命令行,若是命令不須要路徑,或就是項目根目錄,則不須要cwd參數:
findProcess = exec(findStr, {cwd: cmdPath})
// 不受child_process默認的緩衝區大小的使用方法,沒參數也要寫上{}:workerProcess = exec(cmdStr, {})
// 打印正常的後臺可執行程序輸出
findProcess.stdout.on('data', Fn)
// 打印錯誤的後臺可執行程序輸出
findProcess.stderr.on('data', function (data) {
console.log('stderr: find:' + data)
})
// 退出以後的輸出
findProcess.on('close', function (code) {
console.log('out code find:' + code)
})
}
function runKillExec(pid) {
// 執行命令行,若是命令不須要路徑,或就是項目根目錄,則不須要cwd參數:
killProcess = exec(`kill ${pid}`, {cwd: cmdPath})
// 不受child_process默認的緩衝區大小的使用方法,沒參數也要寫上{}:workerProcess = exec(cmdStr, {})
// 打印正常的後臺可執行程序輸出
killProcess.stdout.on('data', data => {
console.log('stderr222: ', data)
})
// 打印錯誤的後臺可執行程序輸出
killProcess.stderr.on('data', function (data) {
console.log('stderr: kill:' + data)
})
// 退出以後的輸出
killProcess.on('close', function (code) {
console.log('out code kill:' + code)
})
}
runFindExec(data => {
let reg = /node([\s\S]+?)IPv4/mg
let res = reg.exec(data)
if (res) {
res = parseFloat(res[1])
console.log('stderr111: ', res)
if (res) {
runKillExec(res)
}
}
})
複製代碼
import outFileBase from './setOutFileBase'
// const exec = require('child_process').exec
const spawn = require('child_process').spawn
let cmdPath = outFileBase.split('/src/App.vue')[0]
console.log('cmdPath', cmdPath)
// 子進程名稱
let workerProcess
function runExec(Fn) {
// 執行命令行,若是命令不須要路徑,或就是項目根目錄,則不須要cwd參數:
workerProcess = spawn('/usr/local/bin/node', [`${cmdPath}/node_modules/.bin/webpack-dev-server`, '--hot', '--port', '1112'], {
cwd: cmdPath
})
// 打印正常的後臺可執行程序輸出
workerProcess.stdout.on('data', Fn)
// 打印錯誤的後臺可執行程序輸出
workerProcess.stderr.on('data', function (data) {
console.log('stderr: npm run dev' + data)
})
// 退出以後的輸出
workerProcess.on('close', function (code) {
console.log('out code:npm run dev' + code)
})
}
export {
runExec
}
複製代碼
npm i codemirror -S
複製代碼
<template>
<div class="in-coder-panel">
<textarea ref="textarea"></textarea>
</div>
</template>
<script>
// 引入全局實例
import _CodeMirror from 'codemirror'
// 核心樣式
import 'codemirror/lib/codemirror.css'
// 引入主題後還須要在 options 中指定主題纔會生效
import 'codemirror/theme/monokai.css'
// 須要引入具體的語法高亮庫纔會有對應的語法高亮效果
// codemirror 官方其實支持經過 /addon/mode/loadmode.js 和 /mode/meta.js 來實現動態加載對應語法高亮庫
// 但 vue 貌似沒有沒法在實例初始化後再動態加載對應 JS ,因此此處才把對應的 JS 提早引入
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/mode/vue/vue.js'
// 嘗試獲取全局實例
const CodeMirror = window.CodeMirror || _CodeMirror
export default {
name: 'in-coder',
props: {
// 外部傳入的內容,用於實現雙向綁定
value: String
},
data () {
return {
// 內部真實的內容
code: '',
// 默認的語法類型
mode: 'x-vue',
// 編輯器實例
coder: null,
// 默認配置
options: {
// 縮進格式
tabSize: 2,
// 主題,對應主題庫 JS 須要提早引入
theme: 'monokai',
// 顯示行號
lineNumbers: true,
line: true,
extraKeys: { 'Ctrl': 'autocomplete' },
hintOptions: {
// 當匹配只有一項的時候是否自動補全
completeSingle: false
}
}
}
},
mounted () {
// 初始化
this._initialize()
},
watch: {
value(data) {
this.coder.setValue(this.value)
}
},
methods: {
// 初始化
_initialize () {
// 初始化編輯器實例,傳入須要被實例化的文本域對象和默認配置
this.coder = CodeMirror.fromTextArea(this.$refs.textarea, this.options)
// 編輯器賦值
this.coder.setValue(this.value || this.code)
// 修改編輯器的語法配置
this.coder.setOption('mode', `text/${this.mode}`)
// 支持雙向綁定
this.coder.on('change', (coder) => {
this.code = coder.getValue()
if (this.$emit) {
this.$emit('input', this.code)
}
})
}
}
}
</script>
<style lang="scss" scoped="" type="text/css">
.in-coder-panel {
width: 375px;
height: 667px;
}
</style>
複製代碼
能夠調整畫布和容器的佈局方式(flex或者block),能夠設置背景色方便觀察。
清除掉以前設置的背景色。
將文件導出到用戶選擇的文件夾。