一套功能相似於有贊商城後臺管理系統中店鋪-微頁面的系統,該系統實現用戶能夠選擇所須要的組件,拖動調整組件的順序,以及編輯組件的內容而且在移動端展現等相關功能,以下圖所示。 css
仔細想了一想,該系統須要實現下面三大功能html
服務端渲染?仍是用原生js封裝組件?使用框架選 react 仍是 vue?(其實我不會react,但請允許我裝個b []~( ̄▽ ̄)~*)vue
由於以前且嘗試開發過element ui庫的組件,詳情點這,對於element ui的架構有必定的瞭解,因而打算把基本的功能照搬element ui的架構,先基於vue開發一套組件系統,其餘的功能再自行定製。react
構建工具我選擇了webpack,大版本爲4.0,試着吃吃螃蟹。先思考下須要webpack的哪些功能:webpack
功能 | 相關插件 |
---|---|
es6-es5 | babel-loader |
sass-css | sass-loader css-loader style-loader |
css文件抽離 | mini-css-extract-plugin |
html模版 | html-loader 以及 html-webpack-plugin |
vue文件解析 | vue-loader |
圖片文件解析 | file-loader |
markdown轉換vue | vue-markdown-loader |
刪除文件 | clean-webpack-plugin 或者腳本 |
熱更新 | HotModuleReplacementPlugin |
webpack配置合併 | webpack-merge |
基本上就是以上的loader及插件了。git
由於組件庫涉及到多個功能的打包,好比組件庫,預覽時的配置,以及後面會提到的和其餘項目的集成,因此對於webpack的配置,能夠學習vue-cli中的配置,將公共的配置抽離,特殊的配置經過webpack-merge插件完成合並。es6
這裏將不一樣的需求及功能抽離成了如上圖所示的幾個webpack配置, 其中webpack.base.js爲公共的配置,以下圖所示,分別處理了vue文件,圖片以及js文件github
這篇文章的目的是主要提供一個思路,因此這裏不詳細講解webpack的相關配置。web
其實對於開發來講,提升效率的主要方式就是將相同的事物封裝起來,就比如一個函數的封裝,這裏由於組件文件的結構是類似的,因此我學習element ui的作法,將組件的建立過程封裝成腳本,運行命令就可以直接生成好文件,以及添加配置文件。代碼以下vue-router
const path = require('path')
const fileSave = require('file-save')
const getUnifiedName = require('../utils').getUnifiedName
const uppercamelcase = require('uppercamelcase')
const config = require('../config')
const component_name = process.argv[2] //組件文件名 橫槓分隔
const ComponentName = uppercamelcase(component_name) //組件帕斯卡拼寫法命名
const componentCnName = process.argv[3] || ComponentName //組件中文名
const componentFnName = '$' + getUnifiedName(component_name) //組件函數名
/** 如下模版字符串不能縮進,不然建立的文件內容排版會混亂 **/
const createFiles = [
{
path: path.join(config.packagesPath, component_name, 'index.js'),
content: `import Vue from 'vue' import ${ComponentName} from './src/main.vue' const Component = Vue.extend(${ComponentName}) ${ComponentName}.install = function(Vue) { Vue.component(${ComponentName}.name, ${ComponentName}) Vue.prototype.${componentFnName} = function() { const instance = new Component() instance.$mount() return instance } } export default ${ComponentName}`
}, {
path: path.join(config.packagesPath, component_name, 'src', 'main.scss'),
content: `@import '~@/style/common/variable.scss'; @import '~@/style/common/mixins.scss'; @import '~@/style/common/functions.scss';`
}, {
path: path.join(config.packagesPath, component_name, 'src', 'main.vue'),
content: `<template> </template> <script> export default { name: '${getUnifiedName(component_name)}' } </script> <style lang="scss" scoped> @import './main.scss'; </style>`
}, {
path: path.join(config.examplesPath, 'src', 'doc', `${component_name}.md`),
content: `## ${ComponentName} ${componentCnName} <div class="example-conainer"> <div class="phone-container"> <div class="phone-screen"> <div class="title"></div> <div class="webview-container"> <sg-${component_name}></sg-${component_name}> </div> </div> </div> <div class="edit-container"> <edit-component> </div> </div> <script> import editComponent from '../components/edit-components/${component_name}' export default { data() { return { } }, components: { editComponent } } </script> `
}, {
path: path.join(config.examplesPath, 'src/components/edit-components', `${component_name}.vue`),
content: ``
}
]
const componentsJson = require(path.join(config.srcPath, 'components.json'))
const docNavConfig = require(path.join(config.examplesPath, 'src', 'router', 'nav.config.json'))
if(docNavConfig[component_name]) {
console.log(`${component_name} 已經存在,請檢查目錄或者components.json文件`)
process.exit(0)
}
if(componentsJson[component_name]) {
console.log(`${component_name} 已經存在,請檢查目錄或者nav.config.json文件`)
process.exit(0)
}
createFiles.forEach(file => {
fileSave(file.path)
.write(file.content, 'utf8')
.end('\n');
})
componentsJson[component_name] = {}
componentsJson[component_name].path = `./packages/${component_name}/index.js`
componentsJson[component_name].cnName = componentCnName
componentsJson[component_name].fnName = componentFnName
componentsJson[component_name].propsData = {}
docNavConfig[component_name] = {}
docNavConfig[component_name].path = `./src/doc/${component_name}.md`
docNavConfig[component_name].cnName = componentCnName
docNavConfig[component_name].vueRouterHref = '/' + component_name
docNavConfig[component_name].fnName = componentFnName
fileSave(path.join(config.srcPath, 'components.json'))
.write(JSON.stringify(componentsJson, null, ' '), 'utf8')
.end('\n');
fileSave(path.join(config.examplesPath, 'src', 'router', 'nav.config.json'))
.write(JSON.stringify(docNavConfig, null, ' '), 'utf8')
.end('\n');
console.log('組件建立完成')
複製代碼
以及刪除組件
const path = require('path')
const fsdo = require('fs-extra')
const fileSave = require('file-save')
const config = require('../config')
const component_name = process.argv[2]
const files = [{
path: path.join(config.packagesPath, component_name),
type: 'dir'
}, {
path: path.join(config.examplesPath, 'src', 'doc', `${component_name}.md`),
type: 'file'
}, {
path: path.join(config.srcPath, 'components.json'),
type: 'json',
key: component_name
}, {
path: path.join(config.examplesPath, 'src', 'router', 'nav.config.json'),
type: 'json',
key: component_name
}]
files.forEach(file => {
switch(file.type) {
case 'dir':
case 'file':
removeFiles(file.path)
break;
case 'json':
deleteJsonItem(file.path, file.key);
break;
default:
console.log('unknow file type')
process.exit(0);
break;
}
})
function removeFiles(path) {
fsdo.removeSync(path)
}
function deleteJsonItem(path, key) {
const targetJson = require(path)
if(targetJson[key]) {
delete targetJson[key]
}
fileSave(path)
.write(JSON.stringify(targetJson, null, ' '), 'utf8')
.end('\n');
}
console.log('組件刪除完成')
複製代碼
用過vue的同窗應該知道vue開發組件有兩種方式,一種是 vue.component()的方式,另外一種是vue.extend()方式,能夠在上面的建立文件代碼中看見,這兩種方式我都用到了。緣由是,對於配置組件的頁面,須要用到動態組件,對於移動端渲染,動態組件確定是不行的,因此須要用到函數形式的組件。
打包vue組件,固然不能將其餘無用的功能打包進庫中,因此再來一套單獨的webpack配置
const path = require('path')
const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base')
const miniCssExtractPlugin = require('mini-css-extract-plugin')
const config = require('./config')
const ENV = process.argv.NODE_ENV
module.exports = merge(webpackBaseConfig, {
output: {
filename: 'senguo.m.ui.js',
path: path.resolve(config.basePath, './dist/ui'),
publicPath: '/dist/ui',
libraryTarget: 'umd'
},
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
}
},
module: {
rules: [
{
test: /\.(sc|c)ss$/,
use: [miniCssExtractPlugin.loader,
{loader: 'css-loader'},
{loader: 'sass-loader'}]
}
]
},
plugins: [
new miniCssExtractPlugin({
filename: "sg-m-ui.css"
})
]
})
複製代碼
先看看組件的入口文件,這是經過配置文件自動生成的,因此沒必要操心什麼,本文的最後會奉上精簡版的vue組件開發webpack腳手架,能夠直接拿去用哦。
//文件從 build/bin/build-entry.js生成
import SgAlert from './packages/alert/index.js'
import SgSwipe from './packages/swipe/index.js'
import SgGoodsList from './packages/goods-list/index.js'
const components = [SgAlert,SgSwipe,SgGoodsList]
const install = function(Vue) {
components.forEach(component => {
component.install(Vue)
})
}
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
// module.exports = {install}
// module.exports.default = module.exports
export default {install}
複製代碼
是否是很簡單啊。
由於開發組件時確定是須要一套webpack的配置用於啓動web服務和熱更新,因此在build文件夾中,編寫了另一套webpack配置用於開發時預覽組件
<--webpack.dev.js-->
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base')
const webpackCleanPlugin = require('clean-webpack-plugin')
const config = require('./config')
module.exports = merge(webpackBaseConfig, {
module: {
rules: [{
test: /\.(sc|c)ss$/,
use: [{
loader: 'style-loader'
}, {
loader: 'vue-style-loader',
}, {
loader: 'css-loader'
}, {
loader: 'sass-loader'
}]
}]
},
devServer: {
host: '0.0.0.0',
publicPath: '/',
hot: true,
},
plugins: [
new webpackCleanPlugin(
['../dist'], {
root: config.basePath,
allowExternal: true
}
),
new webpack.HotModuleReplacementPlugin()
]
})
複製代碼
<--webpack.demo.js-->
const path = require('path')
const merge = require('webpack-merge')
const webpackDevConfig = require('./webpack.dev')
const config = require('./config')
const htmlWebpackPlugin = require('html-webpack-plugin')
const webpackDemoConfig= merge(webpackDevConfig, {
entry: path.resolve(config.examplesPath, 'index.js'),
output: {
filename: 'index.js',
path: path.resolve(config.basePath, './dist'),
publicPath: '/'
},
module: {
rules: [{
test: /\.md$/,
use: [
{
loader: 'vue-loader'
},
{
loader: 'vue-markdown-loader/lib/markdown-compiler',
options: {
raw: true
}
}]
}, {
test: /\.html$/,
use: [{loader: 'html-loader'}]
}, ]
},
plugins: [
new htmlWebpackPlugin({
template: path.join(config.examplesPath, 'index.html'),
inject: 'body'
})
]
})
module.exports = webpackDemoConfig
複製代碼
在其中能夠看見使用了md文件,使用md文件的目的是:
經過vue-markdown-loader就能夠將md文件解析成vue文件了,這個庫是element ui 的官方人員開發的,其實原理很簡單,就是將md文檔先解析成html文檔,再將html文檔放入vue文檔的template標籤內,script 和 style標籤單獨抽離並排放置,就是一個vue的文檔了,解析完後交給vue-loader處理就能夠將md文檔內容渲染到頁面了。
就像上面建立文件那樣,經過配置文件以及腳本動態生成路由文件,運行以前,先建立路由js文件便可
配置文件一覽
{
"main": {
"path": "./src/pages/main.vue",
"cnName": "首頁",
"vueRouterHref": "/main"
},
"alert": {
"path": "./src/doc/alert.md",
"cnName": "警告",
"vueRouterHref": "/alert"
},
"swipe": {
"path": "./src/doc/swipe.md",
"cnName": "輪播",
"vueRouterHref": "/swipe"
},
"goods-list": {
"path":"./src/doc/goods-list.md",
"cnName": "商品列表",
"vueRouterHref": "/goods-list"
}
}
複製代碼
構建完成的路由文件
//文件從 build/bin/build-route.js生成
import Vue from 'vue'
import Router from 'vue-router'
const navConfig = require('./nav.config.json')
import SgMain from '../pages/main.vue'
import SgAlert from '../doc/alert.md'
import SgSwipe from '../doc/swipe.md'
import SgGoodsList from '../doc/goods-list.md'
Vue.use(Router)
const modules = [SgMain,SgAlert,SgSwipe,SgGoodsList]
const routes = []
Object.keys(navConfig).map((value, index) => {
let obj = {}
obj.path = value.vueRouterHref,
obj.component = modules[index]
routes.push(obj)
})
export default new Router({
mode: 'hash',
routes
})
複製代碼
就這樣,從組件的建立到項目的運行都是自動的啦。
固然是用的插件啦,簡單粗暴,看這裏,它是基於Sortable.js封裝的,有贊貌似用的也是這個庫。
但看到右邊的那個編輯框,我不由陷入了沉思,怎麼樣才能作到只開發一次,這個配置頁面就不用管理了?
因爲中文的博大精深,姑且將下面的關鍵字分爲兩種:
分析需求能夠發現,功能組件的內容都是能夠由選項組件編輯的,最初個人想法是,選項組件的內容也根據配置文件生成,好比組件的props數據,這樣就不用開發選項組件了,仔細一想仍是太年輕了,配置項不可能知足設計稿以及不一樣的需求。
只能開發另外一套選擇組件咯,因而乎將選項組件的內容追加到自動生成文件的列表,這樣微微先省點事。
功能組件與選項組件間的通訊可不是一件簡單的事,首先要全部的組件實現同一種通訊方式,其次也不能由於參數的丟失而致使報錯,更重要的是,功能組件在移動端渲染後須要將選項組件配置的選項還原。
嗯,用那些方式好呢?
vuex? 須要對每個組件都添加狀態管理,麻煩
eventBus? 我怕我記不住事件名
props?是個好辦法,可是選項組件要怎麼樣高效的把配置的數據傳遞出來呢?v-model就是一個很優雅的方式
首先功能組件的props與選項組件的v-model綁定同一個model,這樣就能實現高效的通訊,就像這樣:
<--swipe.md-->
## Swipe 輪播
<div class="example-conainer">
<div class="phone-container">
<div class="phone-screen">
<div class="title"></div>
<div class="webview-container" ref="phoneScreen">
<sg-swipe :data="data"></sg-swipe>
</div>
</div>
</div>
<div class="edit-container">
<edit-component v-model="data">
</div>
</div>
<script>
import editComponent from '../components/edit-components/swipe'
export default {
data() {
return {
data: {
imagesList: ['https://aecpm.alicdn.com/simba/img/TB183NQapLM8KJjSZFBSutJHVXa.jpg']
}
}
},
components: {
editComponent
}
}
</script>
複製代碼
就這樣,完美解決組件間通訊,可是這是靜態的組件,別忘了還有一個難點,那就是動態組件該如何進行參數傳遞,以及知道傳遞什麼參數而不會致使報錯。
先看個示例圖
其中左側手機裏的內容是用v-for渲染的動態組件,右側選項組件也是動態組件,這樣就實現了上面所想的,功能組件和選項組件只需開發完成,配置頁面就會自動添加對應的組件,而不用管理,以下圖所示
但這樣就會有一個問題,每一個組件內部的數據不一致,得知道選中的組件是什麼,以及知道該如何傳遞正確的數據,還記得以前的配置文件嗎?其實這些組件也是讀取的配置文件渲染的,配置文件以下:
{
"alert": { // 組件名
"path": "./packages/alert/index.js",
"cnName": "警告",
"fnName": "$SgAlert",
"propsData": {} //props須要傳遞的數據
},
"swipe": {
"path": "./packages/swipe/index.js",
"cnName": "輪播",
"fnName": "$SgSwipe",
"propsData": {
"imagesList": ["https://aecpm.alicdn.com/simba/img/TB183NQapLM8KJjSZFBSutJHVXa.jpg"]
}
},
"goods-list": {
"path": "./packages/goods-list/index.js",
"cnName": "商品列表",
"fnName": "$SgGoodsList",
"propsData": {
}
}
}
複製代碼
每個組件的配置都添加了propsData,裏面的元素和組件props數據以及選項組件v-model關聯,這樣就不用擔憂缺失字段而報錯了,可是這樣的作法給開發添加了麻煩。
組件編寫的過程當中還得將數據手動添加到配置文件,看能不能直接讀取vue文件的props解決這個問題
到了這一步,組件以及組件的編輯拖拽功能均已完成,要考慮的是,如何把編輯拖拽功能頁面集成到現有的後臺系統中去,由於拖拽編輯組件的功能是給客戶用的,這裏爲了效率和組件系統一同開發了。
vue路由的配置,每個路由都對應一個組件,那麼這個系統也能夠這樣作,只須要把中間那部分拖拽配置組件的頁面打包後引入到父工程(商戶後臺管理系統)中去就行了,那麼該如何處理呢?其實很簡單,將webpack打包入口設置成相對應的vue文件就行,就像這樣。
const path = require('path')
const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base')
const config = require('./config')
const miniCssExtractPlugin = require('mini-css-extract-plugin')
const ENV = process.argv.NODE_ENV
module.exports = merge(webpackBaseConfig, {
entry: path.resolve(config.examplesPath, 'src/manage-system-app.vue'),
output: {
filename: 'components-manage.js',
path: path.resolve(config.basePath, './dist/components-manage'),
publicPath: '/dist/components-manage',
libraryTarget: 'umd'
},
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
}
},
module: {
rules: [
{
test: /\.(sc|c)ss$/,
use: [
miniCssExtractPlugin.loader,
{loader: 'css-loader'},
{loader: 'sass-loader'}
]
}
]
},
plugins: [
new miniCssExtractPlugin({
filename: "components-manage.css"
})
]
})
複製代碼
而後在父工程引入組件庫以及樣式文件,再將路由對應的組件配置成這個打包後的js文件就行。
import EditPage from '@/pages/EditPage.js'
new Router({
routes: [{
path: '/edit-page',
components: EditPage
}]
})
複製代碼
這還不簡單麼,看代碼就懂了。
class InsertModule {
constructor(element, componentsData, thatVue) {
if(element instanceof String) {
const el = document.getElementById(element)
this.element = el ? el : document.body
} else if(element instanceof HTMLElement) {
this.element = element
} else {
return console.error('傳入的元素不是一個dom元素id或者dom元素')
}
if(JSON.stringify(componentsData) == '[]') {
return console.error('傳入的組件列表爲空')
}
this.componentsData = componentsData
this.vueInstance = thatVue
this.insertToElement()
}
insertToElement() {
this.componentsData.forEach((component, index) => {
const componentInstance = (this.vueInstance[component.fnName]
&&
this.vueInstance[component.fnName] instanceof Function
&&
this.vueInstance[component.fnName]({propsData: component.propsData})
||
{}
)
if (componentInstance.$el) {
componentInstance.$el.setAttribute('component-index', index)
componentInstance.$el.setAttribute('isComponent', "true")
componentInstance.$el.setAttribute('component-name', component.fnName)
this.element.appendChild(
componentInstance.$el
)
} else {
console.error(`組件 ${component.fnName} 不存在`)
}
})
}
}
const install = function(Vue) {
Vue.prototype.$insertModule = function(element, componentsData) {
const self = this;
return new InsertModule(element, componentsData, self)
}
}
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
export default {install}
複製代碼
這裏將 組件的props數據傳入至組件完成相關配置,這也是以前爲何選擇prosp通訊的緣由
this.vueInstance[component.fnName]({propsData: component.propsData})
<-- swipe.js -->
import Vue from 'vue'
import Swipe from './src/main.vue'
const Component = Vue.extend(Swipe)
Swipe.install = function(Vue) {
Vue.component(Swipe.name, Swipe)
Vue.prototype.$SgSwipe = function(options) {
const instance = new Component({
data: options.data || {},
propsData: {data: options.propsData || {}} //這裏接收了數據
})
instance.$mount()
return instance
}
}
export default Swipe
複製代碼
就係介樣,渲染完成,200元一條的8g內存的夢啥時候可以實現?
最後,奉上此係統精簡版的webpack配置,除了沒拖拽系統以及組件渲染系統,其餘的基本都支持,能夠在此配置上定製本身的功能,編寫本身的組件系統,可是強烈建議閱讀element ui的腳手架配置,嘗試從0-1定製本身的腳手架哦。