可以快速识别.vue文件封装组件插件等功能
sudo npm install @vue/cli -g
sudo npm install -g @vue/cli-service-global
vue serve Home.vue
关闭eslit提示
module.exports = {
    devServer: {
      overlay: {
        warnings: false,
        errors: false
      },
    }
}
<Menu>
    <MenuItem v-for="(item,key) in menuList" :key="key">
        {{item.title}}
    </MenuItem>
    <SubMenu>
        <template #title="title"> 标题1</template>
        <MenuItem>标题1-1</MenuItem>
        <MenuItem>标题1-2</MenuItem>
            <SubMenu>
            <template #title="title">标题1-1-1</template>
            <MenuItem>标题1-1-1-1</MenuItem>
            <MenuItem>标题1-1-1-1</MenuItem>
        </SubMenu>
    </SubMenu>
</Menu>
根据数据递归渲染
menuList:[
    {
        title:'标题1',
        children:[
            {title:'标题1-1'},
            {title:'标题1-2'}
        ]
    },
    {
        title:'标题2',
        children:[
            {title:'标题2-1'},
            {title:'标题2-2'}
        ]
    },
    {
        title:'标题3',
        children:[
            {title:'标题3-1'},
            {title:'标题3-2'}
        ]
    }
]
ReSub组件实现
<Menu>
    <template v-for="(item,key) in menuList" >
        <MenuItem v-if="!item.children" :key="key">
            {{item.title}}
        </MenuItem>
        <ReSub v-else :key="key" :data="item"></ReSub>
    </template>
</Menu>
// ReSub组件主要作用是递归
<template>
    <SubMenu class="sub">
        <template #title="title">{{data.title}}</template>
        <template v-for="d in data.children">
            <MenuItem v-if="!d.children" :key="d.title">{{d.title}}</MenuItem>
            <ReSub v-else :data="d" :key="d.title" class="sub"></ReSub>   
        </template>
    </SubMenu>
</template>
<script>
import SubMenu from './SubMenu';
import MenuItem from './MenuItem'
export default {
    props:['data'], 
    name:'ReSub',
    components:{
        SubMenu,
        MenuItem
    }
}
</script>
vue create <project-name>
可以通过vue ui创建项目/管理项目依赖
vue ui
let path = require('path')
module.exports = {
    publicPath:process.env.NODE_ENV === 'production'? '/vue-project':'/',
    outputDir:'myassets', // 输出路径
    assetsDir:'static', // 生成静态目录的文件夹
    runtimeCompiler: true, // 是否可以使用template模板
    parallel:require('os').cpus().length > 1, //多余1核cpu时 启动并行压缩
    productionSourceMap:false, //生产环境下 不使用soruceMap
    // https://github.com/neutrinojs/webpack-chain
    chainWebpack:config=>{
        // 控制webpack内部配置
        config.resolve.alias.set('component',path.resolve(__dirname,'src/components'));
    },
    // https://github.com/survivejs/webpack-merge
    configureWebpack:{
        // 新增插件等
        plugins:[]
    },
    devServer:{ // 配置代理
        proxy:{
            '/api':{
                target:'http://a.zf.cn:3000',
                changeOrigin:true
            }
        }
    },
    // 第三方插件配置
    pluginOptions: {
        'style-resources-loader': {
            preProcessor: 'less',
            patterns: [
                // 插入全局样式
                path.resolve(__dirname,'src/assets/common.less'), 
            ],
        }
    }
}
<template>
  <div :id="`ball${_uid}`" class="ball" :style="{background:color}">
  </div>
</template>
<script>
export default {
  name:'ScrollBall',
  props:{
      color:{
          type:String,
          default:'red'
      }
  }
}
</script>
<style lang="less">
  .ball{
      width:80px;
      height: 80px;
      border-radius: 50%;
      text-align:center;
      line-height:80px;
  }
</style>
<ScrollBall color="red" :target="500" v-model="pos1"></ScrollBall> 
<ScrollBall color="blue" :target="300" v-model="pos2"></ScrollBall>
<script>
export default {
    props:{
        value:{
            type:Number,
            default:0
        },
        target:{
            type:Number,
            required:true
        }
    },
    mounted(){
        let ele = document.getElementById(`ball${this._uid}`);
        let timer;
        let fn = ()=>{
            let left = this.value + 2;
            if(left > this.target){
                return cancelAnimationFrame(timer);
            }
            ele.style.transform = `translate(${left}px)`;
            this.$emit('input',left);
            timer = requestAnimationFrame(fn)
        }
        timer = requestAnimationFrame(fn)
    }
}
</script>
<ScrollBall color="red" :target="500" v-model="pos">球1</ScrollBall> 
<div :id="`ball${_uid}`" class="ball" :style="{background:color}">
      <slot></slot>
</div>
<ScrollBall color="red" :target="500" v-model="pos1" ref="ball">球1</ScrollBall>
<button @click="stop">stop</button>
stop(){
  this.$refs.ball.stopMove()
}
// 组件中停止小球运动
methods:{
  stopMove(){
      cancelAnimationFrame(this.timer);
  },
  move(){
      let ele = document.getElementById(`ball${this._uid}`);
      let left = this.value + 2;
      if(left > this.target){
          return this.stopMove();
      }
      ele.style.transform = `translate(${left}px)`;
      this.$emit('input',left);
      this.timer = requestAnimationFrame(this.move)
  }
},
mounted(){
  this.timer = requestAnimationFrame(this.move)
}
if(left > this.target){
  this.$emit('end');
  return this.stopMove();
}