從零開始打造一個組件庫--1.編寫一個時間線組件

一.搭建開發環境

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.vuemain.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>

從上圖咱們能夠看到時間線組件包含兩個組件,即timelinetimeline-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>)
        }
    }

以上代碼彷佛不是很好理解,其實也不難理解,首先是獲取reversedirection屬性,接着就是設置類名對象,類型對象包含三個,而後就是獲取該組件的默認插槽內容,判斷若是提供了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

相關文章
相關標籤/搜索