PS:npm速度慢可以使用cnpm
第一步,讓咱們先把項目環境搭建好,首先打開命令窗口,執行以下命令:css
npm init
搭建好了package.json
文件以後,接下來開始裝依賴包,咱們須要用到webpack webpack-cli
來打包項目,執行以下命令:html
npm install webpack webpack-cli --save-dev
在編寫代碼時,咱們須要用到es6
的語法,所以咱們還須要安裝@babel/core @babel/cli @babel/preset-env babel-loader
依賴來處理es6
兼容語法。繼續執行以下命令:vue
npm install --save-dev @babel/core @babel/cli @babel/preset-env babel-loader
接下來,建立一個babel.config.json
文件,而後寫入以下代碼:node
{ "presets": [ [ "@babel/env", { "targets": { "edge": "17", "firefox": "60", "chrome": "67", "safari": "11.1", }, "useBuiltIns": "usage", } ] ] }
這只是一個默認的配置,也能夠自行根據需求來進行配置,更多信息詳見babel文檔。webpack
這尚未結束,咱們還須要搭建vue
的開發環境,咱們須要編譯.vue
,因此咱們須要安裝vue-loader vue-template-compiler vue
等依賴包,繼續執行以下命令:git
npm install vue vue-loader vue-template-compiler --save-dev
咱們目前所須要的依賴就暫時搭建完成,接下來在頁面根目錄建立一個index.html
文件,寫入以下代碼:es6
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>vue-cli</title> </head> <body> <div id="app"></div> </body> <script src="/build.js"></script> </html>
在這裏,咱們注意到了咱們打包最後引入的文件爲build.js
文件,接下來咱們開始編寫webpack
的配置,在根目錄下繼續建立一個webpack.config.js
文件,代碼以下:github
const VueLoaderPlugin = require('vue-loader/lib/plugin'); module.exports = { mode:"development", entry:'./main.js', output:{ path:__dirname, filename:'build.js' }, module:{ rules:[ { test: /\.vue$/, loader: "vue-loader" }, { test: /\.js$/, loader: "babel-loader", exclude: /node_modules/ } ] }, plugins: [ new VueLoaderPlugin() ] }
在導出後面還須要加上這一段js
代碼,以下:web
resolve: { alias: { 'vue': 'vue/dist/vue.js' } }
爲何要加上這一個配置,這個後面會說明緣由,這裏暫時先放置,繼續在根目錄下分別建立一個App.vue
與main.js
文件。代碼分別以下:chrome
import Vue from 'vue'; import App from './App.vue' Vue.config.productionTip = false; var vm = new Vue({ el: "#app", // render:(h) => { return h(App)}, components: { App }, template: "<App />" })
<template> <div id="app"> <p>{{ msg }}</p> </div> </template> <script> export default { data() { return { msg: "hello,vue.js!" }; }, mounted() { }, methods: { } }; </script>
接下來,執行命令webpack
,而後咱們就能夠看到頁面中會生成一個build.js
文件,而後運行index.html
文件,咱們就能夠在瀏覽器頁面上看到hello,vue.js!
的字符串,稍等,咱們彷佛忘記了什麼,通常在開發中,誰會給你運行webpack
命令來打包,不都是執行npm run build
嘛,讓咱們在package.json
中加上這一行代碼
{ "name": "timeline-project", "version": "1.0.0", "description": "a component with vue.js", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" //這裏是添加的代碼 }, "keywords": [ "timeline" ], "author": "", "license": "ISC", "devDependencies": { "@babel/cli": "^7.11.5", "@babel/core": "^7.11.5", "@babel/preset-env": "^7.11.5", "babel-loader": "^8.1.0", "vue": "^2.6.12", "vue-loader": "^15.9.3", "vue-template-compiler": "^2.6.12", "webpack": "^4.44.1", "webpack-cli": "^3.3.12" } }
等等,咱們還忘了一件事,別人均可以使用npm run dev
命令來在本地運行項目,咱們爲何不能夠呢?咱們須要安裝webpack-dev-server
依賴,執行以下命令安裝:
npm install webpack-dev-server --save-dev
安裝完成,讓咱們繼續在package.json
中添加這樣一行代碼,以下所示:
{ "name": "timeline-project", "version": "1.0.0", "description": "a component with vue.js", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", "dev": "webpack-dev-server --open --hot --port 8081" //這裏是添加的代碼 }, "keywords": [ "timeline" ], "author": "", "license": "ISC", "devDependencies": { "@babel/cli": "^7.11.5", "@babel/core": "^7.11.5", "@babel/preset-env": "^7.11.5", "babel-loader": "^8.1.0", "vue": "^2.6.12", "vue-loader": "^15.9.3", "vue-template-compiler": "^2.6.12", "webpack": "^4.44.1", "webpack-cli": "^3.3.12" } }
添加的代碼很好理解,就是啓動服務熱更新,而且端口是8081
。接下來讓咱們嘗試運行npm run dev
,看看發生了什麼!
真的很棒,咱們已經成功運行了vue-cli
項目,搭建環境這一步到目前爲止也算是成功了。
前面還提到一個問題,那就是在webpack.config.js
中爲何要加上resolve
配置,這是由於,若是咱們須要在.vue
文件中使用components
選項來註冊一個組件的話,就必需要引入完整的vue.js
,也就是編譯模板代碼,若是咱們只用render
來建立一個組件,那麼就不須要添加這個配置,這就是官網所說的運行時 + 編譯器 vs. 只包含運行時。
在這裏咱們還要注意一個問題,那就是咱們須要處理單文件組件中的css
樣式,因此咱們須要安裝css-loader與style-loader
依賴。執行以下命令:
npm install style-loader css-loader --save-dev
在webpack.config.js
中添加以下代碼:
const VueLoaderPlugin = require('vue-loader/lib/plugin'); module.exports = { mode:"development", entry:'./main.js', output:{ path:__dirname, filename:'build.js' }, resolve: { // if you don't write this,you can't use components to register component //only use render to register component alias: { 'vue': 'vue/dist/vue.js' } }, module:{ rules:[ { test: /\.vue$/, loader: "vue-loader" }, { test: /\.css$/, loader: ["style-loader","css-loader"] }, { test: /\.js$/, loader: "babel-loader", exclude: /node_modules/ } ] }, plugins: [ new VueLoaderPlugin() ] }
若是是用less
或者stylus
或者scss
,咱們還須要格外安裝依賴,例如less
須要安裝less-loader
,這裏咱們就只用style-loader與css-loader
便可,如對less
等感興趣可自行研究。
特別說明:因爲咱們所編寫的時間線組件並無用到圖標,因此無需添加圖標以及圖片的處理。
時間線組件能夠分紅三部分組成,第一部分即時間線,第二部分即時間戳,第三部分則是內容。咱們先來看時間線組件的一個結構:
<timeline> <timeline-item></timeline-item> </timeline>
從上圖咱們能夠看到時間線組件包含兩個組件,即timeline
與timeline-item
組件,接下來咱們來分析一下組件的屬性有哪些。首先是父組件timeline
組件,根據element ui官方文檔。咱們能夠看到父組件僅僅只提供了一個reverse
屬性,即指定節點排序方向,默認爲正序,但實際上咱們還能夠添加一個屬性,那就是direction
屬性,由於element ui
默認給的時間線組件只有垂直方向,而並無水平方向,所以咱們提供這個屬性來確保時間線組件分爲水平時間線和垂直時間線。
根據以上分析,咱們總結以下:
direction:'vertical' //或'horizontal' reverse:true //或false
接下來,咱們來看子組件的屬性,它包含時間戳,是否顯示時間戳,時間戳位置,節點的類型,節點的圖標,節點的顏色以及節點的尺寸
。這裏咱們暫時忽略圖標這個選項。所以咱們能夠將屬性定義以下:
timestamp:'2020/9/1' //時間戳內容 showTimestamp:true //或false,表示是否顯示時間戳 timestampPlacement:'top' //或'bottom',即時間戳的顯示位置 nodeColor:'#fff'//節點的顏色值 nodeSize:'size' //節點的尺寸 nodeIcon:'el-icon-more' //節點的圖標,在這裏咱們沒有引入element ui組件,所以不添加這個屬性,若是要添加這個屬性,須要先編寫圖標組件
肯定了以上屬性以後,咱們就能夠先來編寫一個靜態的組件元素結構,以下圖所示:
<!-- 父組件結構 --> <div class="timeline"> <!-- 子組件結構 --> <div class="timeline-item"> <!-- 時間線 --> <div class="timeline-item-tail"></div> <!-- 時間線上的節點 --> <div class="timeline-item-node"></div> <!-- 時間線的時間戳與內容 --> <div class="timeline-item-wrapper"> <!-- 時間戳,位置在top--> <div class="timeline-item-timestamp is-top"></div> <!-- 每個時間戳對應的內容 --> <div class="timeline-item-content"></div> <!-- 時間戳,位置在bottom--> <div class="timeline-item-timestamp is-bottom"></div> </div> </div> </div>
根據以上代碼,咱們能夠清晰的看到一個時間線的元素構成,爲了確保佈局方便,咱們多寫幾個子元素,即time-line-item
以及它的全部子元素。接下來,咱們開始編寫靜態的樣式。以下:
.timeline { font-size: 14px; margin: 0; background-color: #ffffff; } .timeline-item { position: relative; padding-bottom: 20px; } .timeline-item-tail { position: absolute; } .is-vertical .timeline-item-tail { border-left: 3px solid #bdbbbb; height: 100%; left: 3px; } .is-horizontal .timeline-item .timeline-item-tail { width: 100%; border-top: 3px solid #bdbbbb; top: 5px; } .is-horizontal:after { content: " "; display: block; height: 0; visibility: hidden; clear: both; } .timeline-item.timeline-item-info .timeline-item-tail { border-color: #44444f; } .timeline-item.timeline-item-info .timeline-item-node { background-color: #44444f; } .timeline-item.timeline-item-info .timeline-item-content { color: #44444f; } .timeline-item.timeline-item-primary .timeline-item-tail { border-color: #2396ef; } .timeline-item.timeline-item-primary .timeline-item-node { background-color: #2396ef; } .timeline-item.timeline-item-primary .timeline-item-content { color: #2396ef; } .timeline-item.timeline-item-success .timeline-item-tail { border-color: #23ef3e; } .timeline-item.timeline-item-success .timeline-item-node { background-color: #23ef3e; } .timeline-item.timeline-item-success .timeline-item-content { color: #23ef3e; } .timeline-item.timeline-item-warning .timeline-item-tail { border-color: #efae23; } .timeline-item.timeline-item-warning .timeline-item-node { background-color: #efae23; } .timeline-item.timeline-item-warning .timeline-item-content { color: #efae23; } .timeline-item.timeline-item-error .timeline-item-tail { border-color: #ef5223; } .timeline-item.timeline-item-error .timeline-item-node { background-color: #ef5223; } .timeline-item.timeline-item-error .timeline-item-content { color: #ef5223; } .is-horizontal .timeline-item { float: left; } .is-horizontal .timeline-item-wrapper { padding-top: 18px; left: -28px; } .timeline-item-node { background-color: #e1e6e6; border-radius: 50%; display: flex; align-items: center; justify-content: center; position: absolute; } .timeline-item-node-normal { width: 12px; height: 12px; left: -2px; } .timeline-item-node-large { width: 14px; height: 14px; left: -4px; } .timeline-item-wrapper { position: relative; top: -3px; padding-left: 28px; } .timeline-item-content { font-size: 12px; color: #dddde0; line-height: 1; } .timeline-item-timestamp { color: #666; } .timeline-item-timestamp.is-top { margin-bottom: 8px; padding-top: 6px; } .timeline-item-timestamp.is-bottom { margin-top: 8px; } .timeline-item:last-child .timeline-item-tail { display: none; }
接下來咱們就要開始實現組件的邏輯封裝了,首先咱們須要封裝timeline
組件,爲了將該組件概括到一個目錄下,咱們先新建一個目錄,叫timeline
,而後新建一個index.vue
文件,而且將咱們編寫好的css
代碼給移到該文件下,如今,你看到該文件的代碼應該以下所示:
<script> export default { name: "timeline" } </script> <style> .timeline { font-size: 14px; margin: 0; background-color: #ffffff; } .timeline-item { position: relative; padding-bottom: 20px; } .timeline-item-tail { position: absolute; } .is-vertical .timeline-item-tail { border-left: 3px solid #bdbbbb; height: 100%; left: 3px; } .is-horizontal .timeline-item .timeline-item-tail { width: 100%; border-top: 3px solid #bdbbbb; top: 5px; } .is-horizontal:after { content: " "; display: block; height: 0; visibility: hidden; clear: both; } .timeline-item.timeline-item-info .timeline-item-tail { border-color: #44444f; } .timeline-item.timeline-item-info .timeline-item-node { background-color: #44444f; } .timeline-item.timeline-item-info .timeline-item-content { color: #44444f; } .timeline-item.timeline-item-primary .timeline-item-tail { border-color: #2396ef; } .timeline-item.timeline-item-primary .timeline-item-node { background-color: #2396ef; } .timeline-item.timeline-item-primary .timeline-item-content { color: #2396ef; } .timeline-item.timeline-item-success .timeline-item-tail { border-color: #23ef3e; } .timeline-item.timeline-item-success .timeline-item-node { background-color: #23ef3e; } .timeline-item.timeline-item-success .timeline-item-content { color: #23ef3e; } .timeline-item.timeline-item-warning .timeline-item-tail { border-color: #efae23; } .timeline-item.timeline-item-warning .timeline-item-node { background-color: #efae23; } .timeline-item.timeline-item-warning .timeline-item-content { color: #efae23; } .timeline-item.timeline-item-error .timeline-item-tail { border-color: #ef5223; } .timeline-item.timeline-item-error .timeline-item-node { background-color: #ef5223; } .timeline-item.timeline-item-error .timeline-item-content { color: #ef5223; } .is-horizontal .timeline-item { float: left; } .is-horizontal .timeline-item-wrapper { padding-top: 18px; left: -28px; } .timeline-item-node { background-color: #e1e6e6; border-radius: 50%; display: flex; align-items: center; justify-content: center; position: absolute; } .timeline-item-node-normal { width: 12px; height: 12px; left: -2px; } .timeline-item-node-large { width: 14px; height: 14px; left: -4px; } .timeline-item-wrapper { position: relative; top: -3px; padding-left: 28px; } .timeline-item-content { font-size: 12px; color: #dddde0; line-height: 1; } .timeline-item-timestamp { color: #666; } .timeline-item-timestamp.is-top { margin-bottom: 8px; padding-top: 6px; } .timeline-item-timestamp.is-bottom { margin-top: 8px; } .timeline-item:last-child .timeline-item-tail { display: none; } </style>
PS:加載sourceMap還須要這樣一個配置
devtool: 'inline-source-map'
爲了確保父子組件共享狀態,咱們利用provide/inject API來傳遞this
對象,以下所示:
export default { name: "timeline", provide(){ return { timeline:this } } }
而後咱們開始定義父組件的屬性,根據前面所述,它包含兩個屬性,所以咱們定義好在props
中,以下所示:
import { oneOf } from '../util' export default { name: "timeline", provide(){ return { timeline:this } }, props:{ reverse:{ type:Boolean, default:false }, direction:{ type:String, default:'vertical', validator:(value) => { return oneOf(['vertical','horizontal'],value,'vertical'); } } } }
上面代碼須要用到一個工具函數oneOf
,顧名思義,就是必須是其中的一項,它有三個參數,第一個參數是匹配的數組,第二個參數是匹配的項,第三個是提供的默認項,該工具函數代碼以下:
export const oneOf = (arr,value,defaultValue) => { return arr.reduce((r,i) => i === value ? i : r,defaultValue); }
其實也不難理解,就是咱們想要的值必須是數組的一項,若是不是就返回默認項,工具函數內部代碼,咱們能夠寫得更清晰明瞭一點,以下所示:
export const oneOf = (arr,value,defaultValue) => { return arr.reduce((result,item) => { return item === value ? item : value; },defaultValue); }
以上的代碼通過簡潔處理就獲得了前面的一行代碼,若是理解不了,能夠採用後者的代碼,至於validator
驗證選項,可參考vue-prop-validator-自定義驗證函數。
接下來,咱們在render
方法中去渲染這個父組件,代碼以下:
import { oneOf } from '../util' export default { name: "timeline", provide(){ return { timeline:this } }, props:{ reverse:{ type:Boolean, default:false }, direction:{ type:String, default:'vertical', validator:(value) => { return oneOf(['vertical','horizontal'],value,'vertical'); } } }, //新添加的內容 render(){ const reverse = this.reverse; const direction = this.direction; const classes = { 'timeline':true, 'is-reverse':reverse, ['is' + direction]:true } const slots = this.$slots.default || []; if(reverse)slots = slots.reverse(); return (<div class={classes}>{slots}</div>) } }
以上代碼彷佛不是很好理解,其實也不難理解,首先是獲取reverse
和direction
屬性,接着就是設置類名對象,類型對象包含三個,而後就是獲取該組件的默認插槽內容,判斷若是提供了reverse
屬性,則調用數組的reverse
方法來讓slots
倒序,在這裏咱們應該很清晰的明白 slots
若是存在,那麼則必定是一個vNode
節點組成的數組,若是沒有,默認就是一個空數組。而後最後返回一個父元素包含該插槽的jsx
元素。
在這裏,咱們使用了jsx
語法,而咱們的項目環境當中還並無添加處理jsx
的依賴,因此咱們須要再次安裝處理jsx
語法的依賴babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx
,並添加相應的配置。繼續在終端輸入如下命令安裝依賴:
npm install babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx --save-dev
而後在babel.config.json
中添加一行配置代碼以下:
"plugins": ["transform-vue-jsx"]
接着在webpack.config.js
中添加一行配置處理以下:
{ test: /\.(jsx?|babel|es6|js)$/, loader: 'babel-loader', exclude: /node_modules/ }
如今,咱們能夠看到頁面中不會報錯,未處理jsx
了。
到此爲止,父組件的邏輯代碼也就完成了,接下來,讓咱們在全局裏面使用一下該組件,看看是否生效。
在main.js
裏面添加以下一行代碼:
import Timeline from './components/timeline/timeline.vue' Vue.component(Timeline.name,Timeline)
而後在App.vue
裏面,咱們使用這個組件,代碼以下:
<template> <div id="app"> <timeline></timeline> </div> </template>
ok,彷佛看起來沒什麼問題,讓咱們繼續編寫子組件的邏輯代碼。
接下來,新建一個item.vue
文件,在該文件中編寫以下代碼:
<template> <div class="timeline-item"> <div class="timeline-item-tail"></div> <div class="timeline-item-node" :class="[`timeline-item-node-${ size || ''}`,`timeline-item-node-${type || ''}`]" :style="{ backgroundColor:color }" v-if="!$slots.dot" ></div> <div class="timeline-item-node" v-if="$slots.dot"> <slot name="dot"></slot> </div> <div class="timeline-item-wrapper"> <div class="timeline-item-timestamp" :class="[`is-`+ timestampPlacement ]" v-if="item.placement === 'top' && showTimestamp" >{{ timestamp }}</div> <div class="timeline-item-content"><slot></slot></div> <div class="timeline-item-timestamp" :class="[`is-`+ timestampPlacement ]" v-if="item.placement === 'bottom' && showTimestamp" >{{ timestamp }}</div> </div> </div> </template> <script> export default { name:"timeline-item", inject:['timeline'], props:{ timestamp:String, showTimestamp:{ type:Boolean, default:false }, timestampPlacement:{ type:String, default:'top', validator:(value) => { return oneOf(['top','bottom'],value,'bottom') } }, type:{ type:String, default:'default', validator:(value) => { return oneOf(['default','info','primary','success','warning','error'],value,'default'); } }, size:{ type:String, default:'normal', validator:(value) => { return oneOf(['normal','large'],value,'normal') } }, color:String } } </script>
編寫完成以後,讓咱們繼續在main.js
中引用它,而後在App.vue
中使用它。代碼分別以下:
//main.js import TimelineItem from './components/timeline/timeline-item.vue' Vue.component(TimelineItem.name,TimelineItem)
<!--App.vue--> <timeline> <timeline-item type="default" size="large" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="top">待審覈</timeline-item> <timeline-item type="info" size="normal" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="bottom">審覈中</timeline-item> <timeline-item type="error" size="large" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="top">審覈失敗</timeline-item> <timeline-item type="success" size="normal" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="bottom">審覈完成</timeline-item> </timeline>
接下來咱們就能夠看到一個時間線已經完美的展現了,時間線組件算是大功告成了。
本文檔已經錄製爲視頻,地址以下:
源碼和文檔已經上傳到github。