一.搭建开发环境
PS:npm速度慢可使用cnpm
第一步,让我们先把项目环境搭建好,首先打开命令窗口,执行如下命令:
npm init
搭建好了package.json
文件之后,接下来开始装依赖包,我们需要用到webpack webpack-cli
来打包项目,执行如下命令:
npm install webpack webpack-cli --save-dev
在编写代码时,我们需要用到es6
的语法,因此我们还需要安装@babel/core @babel/cli @babel/preset-env babel-loader
依赖来处理es6
兼容语法。继续执行如下命令:
npm install --save-dev @babel/core @babel/cli @babel/preset-env babel-loader
接下来,创建一个babel.config.json
文件,然后写入如下代码:
{
"presets": [
[
"@babel/env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1",
},
"useBuiltIns": "usage",
}
]
]
}
这只是一个默认的配置,也可以自行根据需求来进行配置,更多信息详见babel文档。
这还没有结束,我们还需要搭建vue
的开发环境,我们需要编译.vue
,所以我们需要安装vue-loader vue-template-compiler vue
等依赖包,继续执行如下命令:
npm install vue vue-loader vue-template-compiler --save-dev
我们目前所需要的依赖就暂时搭建完成,接下来在页面根目录创建一个index.html
文件,写入如下代码:
<!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
文件,代码如下:
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
代码,如下:
resolve: {
alias: {
'vue': 'vue/dist/vue.js'
}
}
为什么要加上这一个配置,这个后面会说明原因,这里暂时先放置,继续在根目录下分别创建一个App.vue
与main.js
文件。代码分别如下:
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。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。