文章目录
前言
前面已经对基础的vue知识有个简单的解说,接下来就是进阶的任务,学习Vue Cli脚手架和应用。
一、Vue Cli 脚手架
1、使用Vue Cli 的原因
- 在项目当中,我们应该使用的是Vue Cli 脚手架,因为我那可以使用一些跟新更主流的方法。
- 可使用ES6语法。Vue Cli其实就是通过Webpack来搭建开发环境。开发环境当中已经加载了项目中应用的内容,作为开发人员,我们不需要考虑是否要使用ES6语法,因为我们不必要担心浏览器的兼容,在脚手架里通过Loader将ES6打包成ES5,在浏览器可以正常运行,
- 项目是在环境编译,而不是在浏览器编译,因此速度会很快。 当我们编译后浏览器会自动的刷新,方便我们查看。
2、搭建脚手架
Vue Cli脚手架是依赖于Node.js的,因此我们首先需要安装Node环境,具体安装步骤
安装Vue
最新稳定版
$ npm install vue
查看版本
$ vue --version
下载模板,项目初始化。
进入要存放项目的目录,输入以下命令
$ vue init webpack vue-app
结构为
$ vue init webpack 项目名称
安装成功之后,将会提醒你进行下一步的操作。
3、vue-cli目录解析:
- build 文件夹:构建了客户端和服务端的,用于存放 webpack相关配置和脚本。实际的开发中一般不更改它的配置,webpack.base.conf.js 用于配置less、sass等css预编译库,或者配置一下 UI 库。
- config 文件夹:主要存放配置文件,用于区分开发环境、线上环境的不同。常用到此文件夹下 config.js 配置开发环境的 端口号、是否开启热加载 或者设置生产环境的静态资源相对路径、是否开启gzip压缩、
npm run build
命令打包生成静态资源的名称和路径等。 - dist文件夹:默认
npm run build
命令打包生成的静态资源文件,用于生产部署。 - node_modules:存放npm命令下载的开发环境和生产环境的依赖包。
- static 放静态文件。
- index.html:设置项目的一些meta头信息和提供
<div id="app"></div>
用于挂载 vue 节点。 - package.json:用于 node_modules资源部 和 启动、打包项目的 npm 命令管理。
重点文件 src
- src: 存放项目源码及需要引用的资源文件。
- src下assets:存放项目中需要用到的资源文件,css、js、images等。
- src下componets:存放vue开发中一些公共组件:header.vue、footer.vue等。
- src下emit:自己配置的vue集中式事件管理机制。
- src下router:
vue-router
vue路由的配置文件。 - src下service:自己配置的vue请求后台接口方法。
- src下page:存在vue页面组件的文件夹。
- src下util:存放vue开发过程中一些公共的.js方法。
- src下vuex:存放 vuex 为vue专门开发的状态管理器。
- src下app.vue:根组件,使用标签
<route-view></router-view>
渲染整个工程的.vue组件。 - src下main.js:vue-cli工程的入口文件。与index.html文件直接相连的文件。
组件App.vue介绍
App.vue其实是一个公共的组件,里面包含三个部分,分别是:
- 1模板:编写html的结构(注意,模板里面只有一个跟标签)
- 2行为:处理一些业务逻辑,处理数据。
- 3样式:解决页面的样式
执行流程
index.html会执行main.js,而main.js会实例化Vue对象,接下来就会执行App.vue的组件,如果模板有内容,就会往index.html的容器里插入相应的内容。
4、项目初始化
组件化开发,是Vue项目中常见的应用。
我们可以将页面分割成若干份,然后每一份都可右一个或多个组件组成。
在安装完毕之后我们就可以通过npm run dev将项目打开
这里都是组件(components)下HelloWorld.vue的内容,我们将vue文件的模板、行为、样式都删除,留下主要的部分。
App.vue
<template>
<div id="app">
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
</style>
HelloWord.vue
<template>
<div class="hello">
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
}
}
}
</script>
<style scoped>
</style>
二、项目开发相关
1、组件嵌套
在components
下创建一个新的文件Test.vue,写入我们的测试数据
<template>
<div class="test">
{
{title}}
</div>
</template>
<script>
export default {
name: 'test',
data () {
return {
title: "大家好!"
}
}
}
</script>
<style scoped>
</style>
全局注册组件
在main.js内引入Test.vue,这样就可以在App.vue中使用Test的内容
main.js
import Test from "./components/Test"
//全局注册组件
Vue.component("test",Test);
App.vue
<template>
<div id="app">
<test></test>
</div>
</template>
局部注册组件
在App.vue中直接引入Test.vue即可使用
App.vue
<template>
<div id="app">
<test></test>
</div>
</template>
<script>
//局部注册组件
import Test from './components/Test'
export default {
name: 'App',
data() {
return {
title: "这是App的内容"
}
},
components:{
"test" : Test
}
}
</script>
<style>
</style>
2、scoped样式域
如果我们在全局的App.vue下为我们的网页定义样式,按照传统的style样式的话,它就会应用到我们的整个网页当中,包括子组件。那有时候我们想让子组件不收全局样式的影响,我们就可以使用scoped样式域,它会形成一个[data-v-xxxx]
的标识,不同元素设置了标识就会往style中相应的域中去匹配样式。
App.vue,这里将App.vue的h1
样式中color
设置成blue
,span
颜色为绿色,同时使用scoped
样式域
<template>
<div id="app">
<h1>这是App的H1</h1>
<span>这里是App的span内容</span>
<test></test>
</div>
</template>
<script>
//局部注册组件
import Test from './components/Test'
export default {
name: 'App',
data() {
return {
}
},
components:{
"test" : Test
}
}
</script>
<style scoped>
h1{
color: blue;
}
span{
color: green;
}
</style>
Test.vue样式的h1
样式中color
设置成pink
,span
颜色为默认黑色,同时也使用scoped
样式域。
<template>
<div class="test">
<h1>这是Test的H1</h1>
<span>{
{title}}</span>
</div>
</template>
<script>
export default {
name: 'test',
data () {
return {
title: "这里是Test的span内容"
}
}
}
</script>
<style scoped>
h1{
color:pink;
}
</style>
这时候就会得到以下的效果
我们可以看到Test中的span
样式并没有被App.vue中的样式应用,两者的h1
标签样式也是分别得到了不同的效果。
3、一个简单的网页demo
Header.vue
<template>
<header>
<p>{
{title}}</p>
</header>
</template>
<script>
export default {
name: 'app-header',
data () {
return {
title: "这里是header的内容"
}
}
}
</script>
<style scoped>
header{
padding: 10px;
background: lightpink;
}
p{
color: lightslategray;
text-align: center;
}
</style>
Main.vue
<template>
<div class="main">
<h1>Hello Everbody</h1>
<ul>
<li v-for="person in person" v-on:click="person.show = !person.show">
<h2>{
{person.name}}</h2>
<h3 v-show="person.show">{
{person.age}}</h3>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'app-main',
data () {
return {
person: [
{
name: "Jack", age: 21, show: false},
{
name: "Jack", age: 21, show: false},
{
name: "Jack", age: 21, show: false},
{
name: "Jack", age: 21, show: false},
{
name: "Jack", age: 21, show: false},
{
name: "Jack", age: 21, show: false},
{
name: "Jack", age: 21, show: false},
{
name: "Jack", age: 21, show: false},
{
name: "Jack", age: 21, show: false},
{
name: "Jack", age: 21, show: false},
]
}
}
}
</script>
<style scoped>
.main{
width: 100%;
max-width: 1200px;
margin: 40px auto;
padding: 0 20px;
box-sizing: border-box;
}
ul{
display: flex;
flex-wrap: wrap;
list-style-type: none;
padding: 0;
}
li{
flex-grow: 1;
flex-basis: 200px;
text-align: center;
padding: 30px;
border: 1px solid #333;
margin: 10px;
}
</style>
Footer.vue
<template>
<footer>
<p>{
{copyright}}</p>
</footer>
</template>
<script>
export default {
name: 'app-footer',
data () {
return {
copyright: "copyright 2019.11"
}
}
}
</script>
<style scoped>
footer{
padding: 6px;
background: lightslategray;
}
p{
color:lightpink;
text-align: center;
}
</style>
App.vue
<template>
<div id="app">
<app-header></app-header>
<app-main></app-main>
<app-footer></app-footer>
</div>
</template>
<script>
//局部注册组件
import Header from './components/Header'
import Main from './components/Main'
import Footer from './components/Footer'
export default {
name: 'App',
data() {
return {
}
},
components:{
"app-header": Header,
"app-main": Main,
"app-footer": Footer
}
}
</script>
<style scoped>
h1{
color: blue;
}
span{
color: green;
}
</style>
其中Main.vue 下的样式解析
.main {
width: 100%; /* 主体容器的宽度为100% */
max-width: 1200px;/* 最大宽度为1200px */
margin: 40px auto;/* 外边距上下40px,左右auto */
padding: 0 20px;/* 内边距上下为0,左右20px */
box-sizing: border-box;/* box-sizing就不用计算padding */
}
ul {
display: flex;/* li在一行显示 */
flex-wrap: wrap;/* 让内容不超出main容器 */
list-style-type: none;/* 设置li的标记样式为无标记 */
padding: 0;/* 取消父元素的padding值 */
}
li {
flex-grow: 1;/* 如果一行只有一个,就会撑满整行,如果是有两个就两个撑满整行 */
flex-basis: 200px;/* 设置弹性盒元素的伸缩长度为200px */
text-align: center;/* 内容居中,可不用自定义 */
padding: 30px;/* 内边距30px */
border: 1px solid #333;/* 设置边框样式 */
margin: 10px;/* 外边距都是10px */
}
最终效果
4、Vue属性传值Props(父传子)
在上一个demo里我们将Main.vue里面的person
数据放到App.vue的数据。
v-bind:person="person"
(在调用子组件的时候,使用自定义绑定的属性,这样就绑定了一个person
的值,相当于向子组件传入person
的数据。)props:["person"]
(在子组件接受父组件传过来的值,这样就可以使用了,与上面实现的效果相同)
App.vue
<template>
<div id="app">
<app-header></app-header>
<app-main v-bind:person="person"></app-main>
<app-footer></app-footer>
</div>
</template>
<script>
//局部注册组件
import Header from "./components/Header";
import Main from "./components/Main";
import Footer from "./components/Footer";
export default {
name: "App",
data() {
return {
person: [
{
name: "Jack", age: 21, show: false },
{
name: "Jack", age: 21, show: false },
{
name: "Jack", age: 21, show: false },
{
name: "Jack", age: 21, show: false },
{
name: "Jack", age: 21, show: false },
{
name: "Jack", age: 21, show: false },
{
name: "Jack", age: 21, show: false },
{
name: "Jack", age: 21, show: false },
{
name: "Jack", age: 21, show: false },
{
name: "Jack", age: 21, show: false }
]
};
},
components: {
"app-header": Header,
"app-main": Main,
"app-footer": Footer
}
};
</script>
<style scoped>
h1 {
color: blue;
}
span {
color: green;
}
</style>
Main.vue
,props
里面可以接受多个值,里面规范了数据的类型,只有传入的值符合类型,才允许使用该数据。
export default {
name: "app-main",
props: {
person: {
type: Array,
required: true
}
},
data() {
return {
};
}
};
5、理解传值和传引用
上述我们谈到父组件为子组件传值,子组件在接受参数的时候规定了数据的类型,JS中有基本类型值和引用类型值两种。
- JavaScript 变量可以用来保存两种类型的值:基本类型值和引用类型值。基本类型的值源自以下 5 种基本数据类型:
Undefined
、Null
、Boolean
、Number
和String
。 - 引用类型:对象(
Object
)、数组(Array
)、函数(Function
)。引用类型的值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。当复制保存着对象的某个变量时,操作的是对象的引用。但在为对象添加属性时,操作的是实际的对象。
因此如果上述的父组件对多个子组件都传递了person
属性,而且接收的Array
是属于引用类型,我们在一个地方改变的时候,所有的地方都会改变。
(1)传引用
App.vue
<app-main v-bind:person="person"></app-main>
<app-main v-bind:person="person"></app-main>
Main.vue
<button v-on:click="deletePerson">删除</button>
methods: {
deletePerson: function(){
this.person.pop();
}
}
这时候上面和下面的两部分都会执行deletePerson
这个方法
(2)传值
当我们传入一个Number
类型的时候
App.vue
<app-header v-bind:age="age"></app-header>
<app-footer v-bind:age="age"></app-footer>
data() {
return {
age: 21
};
}
在Header.vue和Footer.vue分别都接收了这个值
<p>
{
{title}}
<span v-on:click="changeAge">{
{age}}</span>
</p>
并对Header中21添加一个点击事件,当点击21时,将age
修改为22
export default {
name: 'app-header',
props: {
age:{
type: Number
}
},
data () {
return {
title: "这里是header的内容"
}
},
methods: {
changeAge: function(){
this.age = 22;
}
}
}
可见header和footer的Number
类型的数据不受影响
但是这里会有一个报错的信息
意思是子组件修改父组件的值导致报错,我们应该在这里避免直接修改父组件的值。
解决方案就是在data
里重命名接收的值,比如命名为appAge,在该页面中就可以使用AppAge
这个值了。
<template>
<header>
<p>
{
{title}}
<span v-on:click="changeAge">{
{appAge}}</span>
</p>
</header>
</template>
<script>
export default {
name: 'app-header',
props: {
age:{
type: Number
}
},
data () {
return {
title: "这里是header的内容",
appAge: this.age
}
},
methods: {
changeAge: function(){
this.appAge= 22;
}
}
}
</script>
6、事件传值(子组件传父组件)
我们在点击子组件时,可以向父组件传递事件,子组件通过this.$emit() 注册事件,父组件通过v-on:来接收事件,并且可重新命名后对该事件进行相应的操作。
Header.vue
<template>
<header>
<p v-on:click="changeAge">
{
{title}}
</p>
</header>
</template>
export default {
name: 'app-header',
data () {
return {
title: "这里是header的内容"
}
},
methods: {
changeAge: function(){
this.$emit("changeAge", 22);
}
}
}
App.vue
<p>{
{title}}{
{age}}</p>
<app-header v-on:changeAge="changeAge($event)"></app-header>
export default {
name: "App",
data() {
return {
title: "这里是App.vue 的age:",
age: 21
};
},
methods: {
changeAge(newAge){
this.age = newAge;
}
},
components: {
"app-header": Header,
"app-main": Main,
"app-footer": Footer
}
};
7、Vue的生命周期
New Vue
是实例Vue的对象,没有创建vue实例之前,就会有beforeCreate
这个方法,可是实行加载的操作;- 当进入到
created
当中可以获取一些数据,可以获取结构的一些数据,比如请求网络的接口,把请求的数据赋给属性。后续就可以展示dom,这时候加载就要结束,开始去渲染DOM。 - 当执行完
created
执行完毕后,页面还没被展示出来,继续往下走,判断main.js当中是否有el
?如果有就会检查template
选项是否存在,如果没有就会检查$mount(el)
,
都没有生命周期就会结束。接下来再判断new Vue({ el:’#app’, template: ‘<App/>’, components: { App } }).$mount(el)
template
是组件<App/>
还是html
标签内容,有的话就往下执行,如果都没有生命周期也已经结束了,因为没有可以渲染的东西。 - 进入到下一个钩子函数
befoedMount
,mount就是挂载,现在就是开始编译当前的模板,就是template
的内容,其实是在虚拟DOM中执行,执行完毕后,开始将element
指向的元素往模板放置。 - 进入到
mounted
,已经编译完毕,开始挂载,一旦结束,页面展示。如果想在页面显示出来之前进行操作,就在该阶段进行。
Vue官方文档中的Vue生命周期图示。
下面通过一组提示可以很清楚的知道Vue的生命周期。
beforeCreate: function() {
alert(
"组件实例化之前执行的函数,这时候的data和methods中的数据还没有初始化,所以是不可以在这个阶段使用data中的数据以及methods中的方法"
);
},
created: function() {
alert("组件实例化完毕,但是页面还未显示,data和methods都已经被初始化了,如果想调用methods中的方法或者操作data中的数据,最早是可以在这个阶段操作");
},
beforeMount: function() {
alert("组件挂载前,页面仍然为展示,但是内存中已经编译好了模板,也就是说虚拟DOM已经配置好了");
},
mounted: function() {
alert("组件挂载完毕,如果想要向DOM中插入节点,最早可以在这个阶段进行,此方法执行后,页面展示");
},
beforeUpdate: function() {
alert("组件更新前,页面内的内容仍然没有更新,但是虚拟DOM已经配置好");
},
updated: function() {
alert("组件更新完毕,所有数据都已经保持同步了,此方法执行后,页面新内容显示");
},
beforeDestory: function() {
alert("组件销毁前,这个时候所有的data和methods以及其他内容都是可用的,还没有真正被销毁");
},
destoryed: function() {
alert("组件销毁,所有的data和methods以及其他内容都是不可用的");
}
8、vue路由与请求
往期我们是通过a
标签进行页面的跳转,a
标签有一个属性叫做href
,对应传入地址或者是路径时,a
标签每次点击的时候对发生网络请求,页面都会刷新。
而路由其实与a
标签实现的效果是相同的,但是路由的性能优化的比较好,使用了路由机制,无论点击多少,他都不会请求和刷新页面,直接进入需要进入的地址。
安装路由
npm install vue-router --save-dev
或
cnpm install vue-router --save-dev
main.js引入路由
import Vue from 'vue'
import VueRouter from 'vue-router' //引入路由
import App from './App'
import HelloWorld from './components/HelloWorld'; //引入HelloWorld.vue,一个跳转页面
import Home from './components/Home';//引入Home.vue,后面创建一个新主页,由这个页面跳转到HelloWorld页面
Vue.config.productionTip = false
Vue.use(VueRouter) //使用路由
// 配置路由
const router = new VueRouter({
routes: [
{
path: "/", component: Home},
{
path: "helloworld", component: HelloWorld}
]
})
new Vue({
router, //添加路由
el: '#app',
components: {
App },
template: '<App/>'
})
App.vue中使用路由标签
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
HelloWorld.vue
<template>
<div class="hello">
Hello World
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
}
}
}
</script>
<style scoped>
</style>
这时候我们观察页面的地址带个#号,将之去掉就在main.js配置路由的地方使用mode: "history"
// 配置路由
const router = new VueRouter({
routes: [
{
path: "/", component: Home},
{
path: "helloworld", component: HelloWorld}
],
mode: "history"
})
9、fetch请求
在mounted
钩子函数中请求接口数据
mounted(){
const posts = fetch("http://jsonplaceholder.typicode.com/posts").then(res => {
return res.json();
})
.then(postos => {
console.log(postos);
})
}
提交表单数据
<template>
<div id="window">
<div class="center">
<form @submit.prevent="onSubmit">
<input type="text" v-model="post.title" />
<input type="checkbox" v-model="post.id"/>
<input type="submit" value="提交"/>
</form>
<ul>
<li v-for="post in posts">
<h1>{
{post.title}}</h1>
<p>{
{post.id}}</p>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'HomeWindow',
data() {
return {
posts: [],
post: {
id: null,
title: ""
}
}
},
mounted(){
const posts = fetch("http://jsonplaceholder.typicode.com/posts").then(res => {
// console.log(res);
return res.json();
})
.then(posts => {
this.posts = posts;
})
},
methods: {
onSubmit(){
fetch("http://jsonplaceholder.typicode.com/posts",{
method: "POST",
body: JSON.stringify(this.post),
headers: {
"Content-type": "application/json"
}
}).then(res =>{
return res.json();
}).then(posts => {
this.posts.unshift(posts);
})
}
}
}
</script>
10、Axios请求
安装axios
npm install axios
重新启动可能会有以下问题
出现了以下的错误Refused to load the image ‘http://localhost:8080/favicon.ico’ because it vio…
解决办法:Refused to load the image ‘http://localhost:8080/favicon.ico’
使用axios
在相应的文件中script引入axios
import axios from 'axios'
get方法
axios.get("http://jsonplaceholder.typicode.com/posts").then(res => {
console.log(res.data);
})
post方法
axios.post("http://jsonplaceholder.typicode.com/posts",this.post).then(res => {
console.log(res.data);
})
相比fetch来说axios请求是比较优化简洁的。
11、动态组件与数据缓存
动态组件实现的效果就是在页面进行微小切换的时候,比如div1和div2的内容进行切换。
定义两个不同内容的组件,在home中引入他们
<template>
<div id="div1">
这是Div1 的内容
<input type="text"/>
</div>
</template>
<script>
export default {
name: 'Div1'
}
</script>
<style scoped>
input{
border: 1px #000 solid;
}
</style>
<template>
<div id="div2">
这是Div2 的内容
<input type="text"/>
</div>
</template>
<script>
export default {
name: 'Div2'
}
</script>
<style scoped>
input{
border: 1px #000 solid;
}
</style>
Home.vue引入Div1和Div2,通过:is
来绑定组件,按钮点击后切换两个组件,从而实现动态组件。
<template>
<div id="home">
<component :is="component"></component>
<button @click="component = 'Div1'">Div1</button>
<button @click="component = 'Div2'">Div2</button>
</div>
</template>
<script>
import Div1 from './components/Div1'
import Div2 from './components/Div2'
export default {
name: 'Home',
components: {
Div1,
Div2
},
data(){
return {
component: "Div1"
}
}
}
</script>
<style>
</style>
但是我们发现,如果向Div2输入框中输入内容,再切换到Div1的话,这时候回去看Div2的输入框,内容已经不在了。
也就是说,每一次切换的时候,都会触发依次请求,请求变得很频繁,重新获取组件,这样就会消耗很大的资源。这时候就需要使用缓存.
keep-alive缓存
将组件包裹起来,组件就会实现缓存
<keep-alive>
<component :is="component"></component>
</keep-alive>
12、slot插槽
slot
插槽可以理解为一个占位,在父组件引用子组件时,使用一个子组件标签,这时候如果没有使用插槽,往子组件标签内写入标签是不能展示出来的。
<template>
<div id="homediv">
这是Div 的内容
<input type="text"/>
</div>
</template>
<script>
export default {
name: 'HomeDiv'
}
</script>
<style scoped>
input{
border: 1px #000 solid;
}
</style>
<template>
<div id="home">
<home-div>
<span>这里是home插入的标签</span> <!-- 不被显示 -->
</home-div>
</div>
</template>
<script>
import HomeDiv from './components/HomeDiv'
export default {
name: 'Home',
components: {
HomeDiv
}
}
</script>
<style>
</style>
在子组件中添加插槽,内容就可以通过插槽进行插入(位置可自定义,前或后)
<div id="homediv">
这是Div 的内容
<input type="text"/><br>
<slot></slot>
</div>
使用单纯的插槽占位,只会将父组件传入的所有html
内容往里塞,定义多个就塞多次。
所以说如果需要适用更复杂的操作的话,就需要给各自一个标识,添加slot
属性,以及name
标识
<home-div>
<span slot="span1">这里是home插入的span1</span>
<span slot="span2">这里是home插入的span2</span>
</home-div>
<template>
<div id="homediv">
<slot name="span1"></slot><br>
这是Div 的内容
<input type="text"/><br>
<slot name="span2"></slot>
</div>
</template>
样式域和属性定义区别
-
在上述使用
slot
时,定义样式可以在子组件写,也可在父组件写。 -
而属性定义不同
数据在哪传入,就在哪里定义,我们在父组件Home中向子组件HomeDiv传入一个title
属性,必须在父组件中定义title
这个属性值,不然就会找不到title
这个属性。而在HomeDiv总定义title
是属于子组件中的属性,不属于父组件中的属性。
也就是说标签在哪,属性就需要在哪里定义。
总结
重在积累,加油!