一、axios 的使用
axios 详细资料可以参考axios 中文文档
axios
作为局部模块时,为了使用,需要先进行挂载,挂载的方法有两种:
- 使用
Vue.use();
的方式挂载。 - 使用
Vue.prototype.$axios = axios;
的方式挂载。
1. 基本使用
(1) 简单 GET 请求
var App = {
template: "<div><button @click='getData'>获取数据</button></div>",
methods: {
getData() {
this.$axios.get("http://jsonplaceholder.typicode.com/todos") // GET请求
.then(res => { // 请求成功的处理逻辑
console.log(res.data[0]);
})
.catch(err => { // 请求失败的处理逻辑
console.log(err);
})
}
}
};
Vue.prototype.$axios = axios; // 将axios挂载到Vue实例
new Vue({
el: "#app",
template: '<App/>',
components: { App }
})
(2) 并发请求
var App = {
data() {
return {
getRes: "",
postRes: ""
}
},
template: `
<div>
<div>GET请求响应:{{getRes}}</div>
<div>POST请求响应:{{postRes}}</div>
<button @click="concurrentRequest">并发请求</button>
</div>
`,
methods: {
concurrentRequest() {
this.$axios.defaults.baseURL = "http://jsonplaceholder.typicode.com/"; // 设置请求的baseURL
let getReq = this.$axios.get("posts/55"); // GET请求
let postReq = this.$axios.post("posts", "variable=helloWorld"); // POST请求
this.$axios.all([getReq, postReq])
.then(this.$axios.spread((res1, res2) => {
this.getRes = res1.data;
this.postRes = res2.data;
}))
.catch(err => { // 任意一个请求失败都将导致所有请求不成功
console.log(err);
})
}
}
};
Vue.prototype.$axios = axios; // 挂载axios到Vue实例
new Vue({
el: "#app",
template: '<App/>',
components: { App }
})
2. axios 请求配置
var App = {
template: `
<div>
<div>响应结果:{{getRes}}</div>
<button @click="getData">获取数据</button>
</div>
`,
methods: {
getData() {
this.$axios.defaults.baseURL = "http://jsonplaceholder.typicode.com/posts/";
this.$axios.get('', {
params: {id: 10}, // URL参数
transformResponse: [ // 请求返回后,执行then/catch之前,修改响应数据
function (data) {
console.log("修改之前:", data); // 接收到的data是字符串,需要先转成JSON
data = JSON.parse(data);
data[0].title = "Hello World";
return data;
}
]
})
.then(res => {
console.log(res.data);
})
.catch(err => {
console.log(err);
});
this.$axios.post('', "name=Jack", {
transformRequest: [ // 请求发送之前执行,可以修改请求将要提交的数据。只能用于PUT、POST、PATCH请求中
function (data) {
console.log("修改之前:", data);
data = "name=Rose";
return data;
}
]
})
.then(res => {
console.log(res.data);
})
.catch(err => {
console.log(err);
})
}
}
};
Vue.prototype.$axios = axios; // 挂载 axios
new Vue({
el: "#app",
template: '<App/>',
components: {App}
})
3. axios 拦截器
在请求或响应被then
或catch
处理前可以拦截它们,然后进行业务逻辑处理。
var App = {
template: "<div><button @click='sendRequest'>发送请求</button></div>",
methods: {
sendRequest() {
// 添加请求拦截器
this.$axios.interceptors.request.use(config => {
console.log(config);
// 模拟获取cookie登录状态,并修改请求URL
let userId = localStorage.getItem("userId");
if (userId) { config.url = "65"; }
return config;
}, function (err) {
return Promise.reject(err);
});
// 添加响应拦截器
this.$axios.interceptors.response.use(response => {
console.log(response.data);
// 模拟登录,返回cookie
if (response.data.userId === 6) {
localStorage.setItem('userId', response.data.userId)
}
return response;
}, function (err) {
return Promise.reject(err);
});
this.$axios.defaults.baseURL = "http://jsonplaceholder.typicode.com/posts/";
this.$axios.get("55")
.then(res => { console.log(res); })
.catch(err => { console.log(err); })
}
}
};
Vue.prototype.$axios = axios;
new Vue({
el: "#app",
components: {App},
template: "<App/>"
})
二、vue-router 路由的使用
vue-router官方文档
路由实现方式:
- 传统开发方式
url
改变后,立刻发生请求响应整个页面,有可能资源过多导致页面出现白屏。 - 单页面应用
SPA
(Single Page Application),锚点改变后,不会立刻发送请求,而是在某个合适的时机,发起ajax
请求,页面局部渲染。
1. vue-router 实现原理的简单实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-router</title>
</head>
<body>
<a href="#/login">登录</a>
<a href="#/register">注册</a>
<div id="app"></div>
<script type="text/javascript" src="../node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var objDiv = document.getElementById('app');
window.onhashchange = function() {
console.log(location.hash);
switch(location.hash) {
case '#/login':
objDiv.innerHTML = '<h2>我是登录页面</h2>'
break;
case '#/register':
objDiv.innerHTML = '<h2>我是注册页面</h2>'
break;
default:
objDiv.innerHTML = '<h2>未找到页面</h2>'
break
}
}
</script>
</body>
</html>
2. vue-router 的基本使用
vue-router
的基本使用示例:
Vue.use(VueRouter); // 当Vue不是全局对象时,需要将VueRouter应用到Vue对象上
let Login = {
template: "<div>我是登录页面</div>"
};
let Register = {
template: "<div>我是注册页面</div>"
};
// 创建router对象
var router = new VueRouter({
// 配置路由对象
routes: [
// 路由匹配规则
{
path: "/login",
component: Login
},
{
path: "/register",
component: Register
}
]
});
let App = {
template: `
<div>
<router-link to='/login'>登录页面</router-link>
<router-link to="/register">注册页面</router-link>
<router-view></router-view>
</div>
`
};
new Vue({
el: "#app",
components: {App},
router: router, // 将router路由对象交给Vue实例管理
template: "<App/>"
})
- 引入
vue-router
模块后,会有两个全局的组件router-link
、router-view
router-link
相当于<a>
标签,它的to
属性相当于<a>
标签的href
属性;router-view
是路由匹配组件的出口。
3. 命名路由
命名路由就是给路由规则添加name
属性,然后将router-link
的to
属性改为动态绑定。
Vue.use(VueRouter);
let Login = {
template: "<div>我是登录页面</div>"
};
let Register = {
template: "<div>我是注册页面</div>"
};
// 创建router对象
var router = new VueRouter({
routes: [
{
name: "login", // 路由命名
path: "/login",
component: Login
},
{
name: "register", // 路由命名
path: "/register",
component: Register
}
]
});
let App = {
template: `
<div>
<router-link :to="{name: 'login'}">登录页面</router-link>
<router-link :to="{name: 'register'}">注册页面</router-link>
<router-view></router-view>
</div>
`
};
new Vue({
el: "#app",
components: {App},
router: router, // 将router路由对象交给Vue实例管理
template: "<App/>"
})
4. 路由参数
路由参数包括:1. 动态路由参数;2. URL参数。
let UserParams = {
template: "<div>动态路由参数页面</div>",
created() {
// VueRouter引入之后,Vue实例上会挂载有$router、$route两个属性对象,
// 组件会继承Vue实例上的$router、$route对象;通过这两个对象,可以在组件内部获得路由参数。
console.log(this.$router);
console.log(this.$route.params)
}
};
let UserQuery = {
template: "<div>URL参数页面</div>"
};
// 创建router对象
var router = new VueRouter({
routes: [
{
name: "UserParams",
path: "/user/:id", // 动态路由参数,以冒号开头
component: UserParams
},
{
name: "UserQuery",
path: "/UserQuery",
component: UserQuery
}
]
});
let App = {
// 两种路由参数传入 router-link 的示例:
// 动态路由参数通过 params 属性选项传入参数;URL参数通过 query 属性选项传入参数。
template: `
<div>
<router-link :to="{name: 'UserParams', params: {id: 2}}">动态路由参数</router-link>
<router-link :to="{name: 'UserQuery', query: {userid: 3}}">URL参数</router-link>
<router-view></router-view>
</div>
`
};
new Vue({
el: "#app",
components: {
App
},
router: router,
template: "<App/>"
})
当使用路由参数时,例如从/user/foo
导航到/user/bar
,原来的组件实例会被复用。因为两个路由都渲染同一个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。
如果在复用组件时,想对路由参数的变化作出响应的话,可以简单地 watch (监测变化) $route
对象:
const User = {
template: '...',
watch: {
'$route' (to, from) {
// 对路由变化作出响应...
}
}
}
或者使用 2.2 中引入的 beforeRouteUpdate
导航守卫:
const User = {
template: '...',
beforeRouteUpdate (to, from, next) {
// react to route changes...
// don't forget to call next()
}
}
5. 嵌套路由
在路由对象router
中通过children
属性,定义子路由。
let Song = {
template: "<div>歌曲内容页</div>"
};
let Movie = {
template: "<div>影视内容页</div>"
};
let Home = {
template: `
<div>
首页内容
<br />
<router-link :to="{name: 'song'}">歌曲</router-link>
<router-link :to="{name: 'movie'}">影视</router-link>
<router-view></router-view>
</div>
`
};
// 创建router对象
var router = new VueRouter({
routes: [
{
name: "home",
path: "/home",
component: Home,
children: [
{
name: 'song',
path: 'song',
component: Song
},
{
name: 'movie',
path: 'movie',
component: Movie
}
]
}
]
});
let App = {
// 两种路由参数传入 router-link 的示例
template: `
<div>
<router-link :to="{name: 'home'}">首页</router-link>
<router-view></router-view>
</div>
`
};
new Vue({
el: "#app",
components: {
App
},
router: router,
template: "<App/>"
})
6. keep-alive 在路由中的使用
内置组件keep-alive
可以将组件的状态缓存,当路由切换后可以保持路由时加载的组件的状态。
let Timeline = {
template: "<div><h3>这是首页组件</h3></div>",
created() { console.log('首页组件创建了'); },
mounted() { console.log('首页组件DOM加载了'); },
destroyed() { console.log('首页组件销毁了'); }
};
let Pins = {
template: "<div><h3 @click="clickHandler">这是沸点组件</h3></div>",
methods: {
clickHandler(e) { e.target.style.color = 'red'; }
},
created() { console.log('沸点组件创建了'); },
mounted() { console.log('沸点组件DOM加载了'); },
destroyed() { console.log('沸点组件销毁了'); }
};
let router = new VueRouter({
routes: [
{
path: '/timeline',
component: Timeline
},
{
path: '/pins',
component: Pins
}
]
});
let App = {
template: `
<div>
<router-link to="/timeline">首页</router-link>
<router-link to="/pins">沸点</router-link>
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
`
};
new Vue({
el: "#app",
router,
components: {App},
template: "<App/>"
})
7. 在路由中通过 meta 进行权限控制
示例代码知识点总结:
- 可以将某些数据保存到本地的 localStorage 中。
- 在方法中,可以通过编程式路由跳转到指定页面。
- 给路由设置
meta
属性,以规定该路由是否需要权限验证。 - 在全局前置导航守卫中执行路由权限验证的逻辑。
- 必须调用全局前置导航守卫中的
next()
方法,否则页面不会跳转。
let Home = {template: "<div>这是首页</div>"};
let Blog = {template: "<div>这是博客</div>"};
let Login = {
data() {
return {name: "", passwd: ""}
},
template: `
<div>
<input type="text" v-model="name">
<input type="password" v-model="passwd">
<input type="button" value="登录" @click="loginHandler">
</div>
`,
methods: {
loginHandler() {
// 将数据保存到本地的 localStorage 中,以模拟登录
localStorage.setItem("user", {name: this.name, passwd: this.passwd});
// 通过编程式导航跳转到目标页面
this.$router.push({
name: "blog"
})
}
}
};
const router = new VueRouter({
routes: [
{
path: "/",
redirect: "/home"
},
{
path: "/home",
component: Home
},
{
path: "/blog",
name: "blog",
component: Blog,
// 给路由做权限控制
meta: {
// 规定这个路由是否需要登录
authValidate: true
}
},
{
path: "/login",
name: "login",
component: Login
}
]
});
router.beforeEach((to, from, next) => {
console.log(to);
console.log(from);
// 通过目的路由的meta属性来判断组件是否设定了权限验证
if (to.meta.authValidate) { // 路由到有登录验证的组件时执行
if (localStorage.getItem("user")) { // 判断是否已经登录,若已登录,则直接放行
next();
} else {
next({ // 若未登录,则跳转到登录页面
path: '/login'
});
}
} else { // 路由到没有登录验证的组件时执行
if (localStorage.getItem("user")) {
if (to.name === "login") {
console.log(to.name);
next({
path: "/home"
})
}else {
next();
}
} else {
next();
}
}
});
new Vue({
el: "#app",
router,
template: `
<div>
<router-link to="/home">首页</router-link>
<router-link to="/blog">我的博客</router-link>
<router-link to="/login">登录</router-link>
<a href="javascript: void(0)">退出</a>
<router-view></router-view>
</div>
`
})
8. vue-router 导航完成之后异步获取数据
需求:在导航完成之后加载数据,渲染DOM
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-router在导航完成后获取数据</title>
</head>
<body>
<div id="app"></div>
<script src="node_modules/vue/dist/vue.js"></script>
<script src="node_modules/vue-router/dist/vue-router.js"></script>
<script src="node_modules/axios/dist/axios.js"></script>
<script>
// 导航完成后获取数据,这让我们有机会在数据获取期间展示一个loading状态,还可以在不同视图间展示不同的loading状态。
var Index = {
template: "<div>我是首页</div>"
};
var Post = {
data() {
return {
loading: false,
error: null,
post: null
}
},
template: `
<div>
<div class="loading" v-if="loading">
loading ...
</div>
<div class="error" v-if="error">
{{error}}
</div>
<div class="content" v-if="post">
<h2>{{post.title}}</h2>
<p>{{post.body}}</p>
</div>
</div>
`,
created() {
// 组件创建完成后获取数据,此时data已经被监听了
this.fetchData();
},
// watch: {
// "$route": 'fetchData'
// },
methods: {
fetchData(){
console.log("method fetchData is run");
this.error = null;
this.post = null;
this.loading = true;
this.$axios.get("https://jsonplaceholder.typicode.com/posts/2")
.then(res=> {
this.loading = false;
console.log(res.data);
this.post = res.data;
})
.catch(err=> {
this.err = err.toString();
})
}
}
};
var router = new VueRouter({
routes: [
{
path: '/index',
name: 'index',
component: Index
},
{
path: '/post',
name: 'post',
component: Post
}
]
});
var App = {
template: `
<div>
<router-link :to="{name: 'index'}">首页</router-link>
<router-link :to="{name: 'post'}">我的博客</router-link>
<router-view></router-view>
</div>
`
};
Vue.prototype.$axios = axios;
var ap = new Vue({
el: "#app",
data: { },
components: { App },
template: '<App/>',
router
});
</script>
</body>
</html>
9. vue-router导航守卫之在导航完成前获取数据
导航守卫官方文档
有三种方法实现导航完成前获取数据:
- 通过vue-router全局守卫
beforeEach
; - 通过
watch
属性侦听$route
的变化; - 通过vue-router的组件内守卫
beforeUpdate
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-router导航守卫之在导航完成前获取数据</title>
</head>
<body>
<div id="app"></div>
<script src="node_modules/vue/dist/vue.js"></script>
<script src="node_modules/vue-router/dist/vue-router.js"></script>
<script src="node_modules/axios/dist/axios.js"></script>
<script>
Vue.use(vueRouter);
var User = {
data() {
return {
user: '',
error: null,
msg: '', // 输入框中输入的内容
msg1: '', // 页面中显示的数据
confir: true
}
},
template: `
<div>
<input type="text" v-model="msg">
<p>{{msg1}}</p>
<button>保存</button>
<div v-if="error" class="error">
{{error}}
</div>
<div class="user" v-if="user">
<h2>{{user}}</h2>
</div>
</div>
`,
methods: {
setDatas(data) {
this.user = data;
},
setError(err) {
this.error = err;
},
saveData(){
this.msg1 = this.msg;
this.msg = '';
this.confir = true
}
},
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
axios.get('http://127.0.0.1:8080/user/${to.params.id}')
.then(res => {
next(vm => {
vm.setDatas(res.data)
});
})
.catch(err => {
next(vm => vm.setError(err))
})
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
this.user = null;
this.$axios.get('http://127.0.0.1:8080/user/${to.params.id}')
.then(res => {
this.setDatas(res.data);
next();
})
.catch(err => {
this.setError(err);
next();
});
next();
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
// 示例代码:离开组件前检查用户的输入是否保存
if (this.msg && this.confir === true) {
// 提示用户保存信息
this.confir = confirm('请保存数据');
next(false) // 表示不放行路由;但必须加这句代码,否则会阻塞
}else {
next();
}
}
};
var Test = {
template: '<div>这是测试组件</div>'
};
// 路由设置
var router = new VueRouter({
routes: [
{
path: '/user/:id',
name: 'user'
},
{
path: '/test',
name: 'test',
component: Test
}
]
});
// 入口组件
var App = {
template: `
<div>
<router-link :to="{name: 'test'}">测试</router-link>
<router-link :to="{name: 'user', params: {id: 1}}">用户1</router-link>
<router-link :to="{name: 'user', params: {id: 2}}">用户2</router-link>
<router-view></router-view>
</div>
`
}
Vue.prototype.$axios = axios;
new Vue({
el: "#app",
data: {},
components: {App},
template: '<App/>',
router
})
</script>
</body>
</html>
三、webpack 入门
1. 安装
- 执行命令
npm init
初始化; - 执行命令
npm install [email protected] -D
下载webpack
。
2. 简单使用
手动实现一个简易
vue-cli
脚手架工具,同时学习webpack
的使用。
(1) 创建index.html
文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<script src="./dist/build.js"></script>
</body>
</html>
(2) 创建main.js
作为项目的入口文件
// ECMAScript6的模块导入
import Vue from "vue/dist/vue"
import App from "./App.js"
import {num1, num2, add} from "./App.js"
// 导入模块时,还可以: import * as app from "./App.js"
// 在调用时,通过: app.num1; app.add; app.default
console.log(num1, num2);
console.log(add(3, 6));
new Vue({
el: "#app",
components: {App},
template: "<App/>"
})
(3) 创建App.js
组件文件
var app = {
template: "<div>程序入口组件</div>"
};
// 三种抛出方式
export default app; // 直接抛出
export var num1= 1; // 声明并抛出
var num2 = 2; export {num2}; // 先声明,再抛出
export function add(x, y) { // 抛出一个函数
return x + y;
}
(4) 打包
1. 如果 npm 全局安装 webpack,可以执行命令`webpack ./main.js ./dist/build.js`。
2. 如果`webpack`安装在项目目录,可以按如下进行配置使用:
- 设置"package.json"文件`scripts`属性"build": "webpack ./main.js ./dist/build.js";
- 执行命令`npm run build`进行打包。
(5) build.js
文件解读
build.js 文件中有"0~6"注释的编号,它们分别是:
- 0: 设置一个全局变量,在 web 端指向
window
对象; - 1: 载入
main.js
的代码,一个Vue
实例对象; - 2: 载入
vue
源码本身; - 3,4,5: 都是与
node_modules/setimmediate
(Vue 的 DOM 异步更新)相关; - 6: 与
App.js
解析相关。
3. webpack 打包执行顺序
- 把所有模块的代码放到函数中,用一个数组保存起来;
- 根据
require
时传入的数组索引,能知道需要哪一段代码; - 从数组中,根据索引取出包含我们代码的函数;
- 执行该函数,传入一个对象
module.exports
; - 我们的代码,按照约定,正好是用
module.exports = 'xxx'
进行赋值; - 调用函数结束后,
module.exports
从原来的空对象,就有值了; - 最终
return module.exports;
作为require
函数的返回值。
4. webpack 配置文件
webpack 可以以通过指定配置文件的方式去执行打包。
- 当全局安装
webpack
,且配置文件名称为预设的webpack.config.js
时,可以通过执行命令:webpack
打包; - 当全局安装
webpack
,但配置文件名称非预设时,可以通过执行命令:webpack --config <配置文件路径>
打包。 - 当非全局安装
webpack
,可以将打包命令webpack --config <配置文件路径>
写入到 package.json 文件scripts
属性中。
webpack 配置文件说明:
var path = require('path') // node.js语句
module.exports = {
// 入口
entry: {
// 可以有多个入口,也可以只有一个
// 如果只有一个,就默认从这个入口开始解析
"main": "./main.js"
},
output: {
path: path.resolve('./dist'), // 相对路径转绝对路径
filename: "./build.js"
},
watch: true // 监视文件改动,自动打包成build.js
};
四、webpack 解析器和插件
webpack 在打包过程中遇到各种不同的文件时,会需要不同的解析器去解析相应的文件。例如:遇到.css
文件时,需要用到css-loader
和style-loader
。解析器需要配置到webpack
配置文件的module
属性里。
1. CSS 文件处理
- ES6模块导入语法:
.css
文件的导入语句是import 'xxx.css'
。 - 解析器下载:在命令行窗口执行命令
npm i css-loader style-loader -D
下载。 - 配置
webpack
配置文件:
var path = require('path')
module.exports = {
entry: {
"main": "./main.js"
},
output: {
path: path.resolve('./dist'),
filename: "./build.js"
},
// 声明模块 包含各个loader
module: {
loaders: [
{ // 添加处理css文件的loader
test: /\.css$/,
loader: 'style-loader!css-loader' // 先用css-loader解析,后用style-loader载入
}
]
},
watch: true
};
webpack 在打包过程中,遇到.css
文件,会先用css-loader
解析器去解析这个文件,然后用style-loader
解析器生成 style 标签,并放到 head 标签里。
2. less 文件处理
- ES6模块导入语法:
.less
文件的导入语句是import 'xxx.less'
。 - less 模块下载:在命令行执行命令
npm i less -D
下载。 - 解析器下载:在命令行窗口执行命令
npm i less-loader -D
下载。 - 配置
webpack
配置文件:
var path = require('path')
module.exports = {
entry: {
"main": "./main.js"
},
output: {
path: path.resolve('./dist'),
filename: "./build.js"
},
module: {
loaders: [
{ // 添加处理css文件的loader
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{ // 添加处理less文件的loader
test: /\.less$/,
loader: 'style-loader!css-loader!less-loader'
}
]
},
watch: true
}
3. 图片文件处理
- ES6模块导入语法:图片文件的导入语句是
import imgSrc from 'xxx.jpg'
。 - 解析器下载:在命令行窗口执行命令
npm i url-loader file-loader -D
下载。 - 配置
webpack
配置文件:
module.exports = {
entry: {"main": "./main.js"},
output: {filename: "./dist/build.js"},
// 声明模块 包含各个loader
module: {
loaders: [
{ // css文件处理
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{ // less文件处理
test: /\.less$/,
loader: 'style-loader!css-loader!less-loader'
},
{ // 图片文件处理
test: /\.(jpg|png|jpeg|gif|svg)$/,
loader: 'url-loader?limit=4000'
}
]
}
};
图片大小比limit
设置的值小时,html 页面中会使用base64
编码载入图片,这可以减少图片的网络请求;图片大小比limit
设置的值大时,会生成一个图片副本,html 页面中图片的路径指向该副本,图片副本会和 html 页面混在一起,导致项目的代码结构混乱;因此设置一个合理的limit
值是很有必要的。
特别说明:
webpack 最终会将各个模块打包成一个文件,因此样式中的url
路径是相对于入口 html 页面的,而不是相对于原始 CSS 文件所在路径的,这就会导致引入失败。这个问题是通过配置file-loader
解决的,file-loader
可以解析项目中的url
引入(不仅限于 CSS 文件),然后根据配置将文件复制到相应的路径,修改打包后文件的引用路径。
4. html 文件插件
- 插件下载:在命令行窗口执行命令
npm i html-webpack-plugin --save-dev
下载。 - 配置
webpack
配置文件:
var path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 载入插件对象
module.exports = {
entry: {
"main": "./src/main.js"
},
output: {
path: path.resolve('./dist'),
filename: "./build.js"
},
module: {
loaders: [
{
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{
test: /\.less$/,
loader: 'style-loader!css-loader!less-loader'
},
{
test: /\.(jpg|png|jpeg|gif|svg)$/,
loader: 'url-loader?limit=400000'
}
]
},
// 声明插件
plugins: [
new HtmlWebpackPlugin({ // 生成html文件的插件
template: './src/index.html' // html源文件
})
],
watch: true
};
5. webpack-dev-server 热加载插件
- 插件下载:在命令行窗口执行命令
npm install webpack-dev-server --save-dev
下载。 - 常用配置参数:
- --open 自动打开浏览器
- --hot 热更新,不刷新替换 css 样式
- --inline 自动刷新
- --port
指定端口 - --process 显示编译进度
webpack-dev-server
插件的配置,需要写在package.json
文件中:
{
"scripts": {
"dev": "webpack-dev-server --open --hot --inline --config ./webpack.dev.config.js"
}
}
6. ES6 语法解析
- 模块下载:在命令行执行命令
npm i babel-core babel-loader babel-preset-dev babel-plugin-transform-runtime -D
- 配置
webpack
配置文件:
var path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 载入插件对象
module.exports = {
entry: {
"main": "./src/main.js"
},
output: {
path: path.resolve('./dist'),
filename: "./build.js"
},
module: {
loaders: [
{ // css文件处理
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{ // less文件处理
test: /\.less$/,
loader: 'style-loader!css-loader!less-loader'
},
{ // 图片文件处理
test: /\.(jpg|png|jpeg|gif|svg)$/,
loader: 'url-loader?limit=400000'
},
{
// 处理ES6,7,8
test: /\.js$/,
loader: 'babel-loader',
exclude: '/node_modules/', // 排除对node_modules的解析
options: {
presets: ['env'], // 处理关键字
plugins: ['transform-runtime'] // 处理函数
}
}
]
},
// 声明插件
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html' // html源文件
})
],
watch: true
};
ES6 语法解析模块介绍:
1)babel-core
:
babel-core 的作用是把 js 代码分析成 ast(抽象语法树),方便各个插件分析语法进行相应的处理。有些新语法在低版本 js 中是不存在的,如箭头函数、rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 ast,分析其语法后再转为低版本 js。
2)babel-loader
:
babel-core 会使用 abel 转译器,abel 转译器提供了 babel 的转译API,如 babel.transform 等,用于对代码进行转译。abel 转译器通过 babel-loader 调用这些 API 来完成将 ES6 代码进行转译。所以 babel-core 和 babel-loader 需要联合使用。
3)babel-preset-env
:
自行配置转译过程中使用的各类插件非常麻烦,所有 babel 官方帮我们做了一些预设的插件集,称之为preset
。这样我们只需要使用对应的 preset 就可以了。以 JS 标准为例,babel 提供了: es2015、es2016、es2017、env。es20xx 的 preset 只转译该年份批准的标准;env 代指最新的标准,包括了 latest 和 es20xx 各年份。
4)babel-plugin-transform-runtime
:
babel 默认只转换新的 JavaScript 语法,而不转换新的 API。像Iterator
,Generator
,Set
,Maps
,Proxy
,Reflect
,Symbol
,Promise
等全局对象,以及一些定义在全局对象上的方法(如Object.assign
)都不会转译。如果想使用这些新的对象和方法,必须使用 babel-polyfill 模块,为当前环境提供一个垫片。
7. vue组件单文件引入
模块下载:在命令行窗口执行命令
npm install [email protected] [email protected] -D
下载。vue-loader
依赖于vue-template-compiler
。- 创建App.vue文件:
<template>
<!-- 当前组件的HTML结构 -->
<div>
{{msg}}
</div>
</template>
<script>
// 当前组件的业务逻辑
export default {
name: "App",
data(){
return {
msg: 'hello App.vue'
}
}
}
</script>
<style scoped>
/* 当前组件的样式 */
</style>
- 创建入口文件main.js:
import Vue from "vue"
import App from "./App.vue"
new Vue({
el: "#app",
render: c => c(App)
});
Render
函数是Vue2.x版本新增的一个函数。它基于 JavaScript 计算,使用虚拟 DOM 来渲染节点提升性能。通过使用createElement(h)
来创建 DOM 节点,createElement
是render
的核心方法。Vue 编译的时候会把 template 里面的节点解析成虚拟 DOM。
- 配置
webpack
配置文件:
var path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
"main": "./src/main.js"
},
output: {
path: path.resolve('./dist'),
filename: "./build.js"
},
module: {
loaders: [
{
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{
test: /\.less$/,
loader: 'style-loader!css-loader!less-loader'
},
{
test: /\.(jpg|png|jpeg|gif|svg)$/,
loader: 'url-loader?limit=400000'
},
{
// 处理vue单文件组件
test:/\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
watch: true
};
8. CommonsChunkPlugin 的使用
CommonsChunkPlugin 主要是用来提取第三方库和公共模块,避免首屏加载的 bundle 文件或者按需加载的 bundle 文件体积过大,从而导致加载时间过长,着实是优化的一把利器。
(1) chunk(代码块)的分类
- webpack当中配置的入口文件(entry)是 chunk,可以理解为
entry chunk
; - 入口文件以及它的依赖文件通过 code splite (代码分割)出来的也是 chunk,可以理解为
children chunk
; - 通过 CommonsChunkPlugin 创建出来的文件也是 chunk,可以理解为
commons chunk
(2) CommonsChunkPlugin 可配置的属性
- name:可以是已经存在的 chunk (一般指入口文件)对应的
name
,那么就会把公共模块代码合并到这个 chunk 上 ;否则,会创建名字为name
的commons chunk
进行合并。 - filename:指定
commons chunk
的文件名。 - chunks:指定
source chunk
,即指定从那些 chunk 当中去找公共模块,省略该选项的时候,默认就是entry chunk
。 - minChunks:既可以是数字,也可以是函数,还可以是 Infinity,具体用法和区别下面讨论。
(3) 代码块分离的三种情况
package.json
中的dependences
属性记录了项目中依赖的第三方库。使用模块下载命令npm install vue.js -D
会将模块添加到该属性中。
示例背景说明:项目依赖第三方库 Vue.js;两个入口文件 main1.js、main2.js;入口文件都用到了自定义公共模块 common.js。
[1] 不分离出第三方库和自定义公共模块
修改 webpack.config.js 配置文件
const path = require('path');
module.exports = {
entry: { // 多入口文件的配置
"main1": "./src/main1.js",
"main2": "./src/main2.js"
},
output: {
path: path.resolve('./dist'),
filename: "[name].js" // 对应多入口的多出口配置
},
watch: true
};
此时,第三方库和自定义公共模块会被打包到所有入口文件中,造成代码冗余及重复加载。
[2] 分离出第三方库、自定义公共模块、webpack运行文件,但他们在同一个文件中
修改 webpack.config.js 配置文件,新增一个入口文件 vendor,并添加 CommonsChunkPlugin 插件进行模块提取分离:
const path = require('path');
const webpack = require('webpack'); // 导入webpack运行文件
const packagejson = require('./package.json'); // 导入项目package.json文件
module.exports = {
entry: { // 多入口文件的配置
"main1": "./src/main1.js",
"main2": "./src/main2.js",
"vendor": Object.keys(packagejson.dependencies) // 获取生产环境依赖的库
},
output: {
path: path.resolve('./dist'),
filename: "[name].js" // 对应多入口的多出口配置
},
watch: true,
plugins: [
new webpack.optimize.CommonsChunkPlugin({ // 模块提取分离到vendor.js文件中
name: ['vendor'],
filename: '[name].js'
})
]
};
此时第三方库、自定义公共模块、webpack运行文件被分离到同一个文件中。但是每次打包时,webpack 运行文件都会变,如果不分离出 webpack 运行文件,每次打包生成 vendor.js 对应的哈希值都会变化,使浏览器认为缓存的 vendor.js失效,而重新去服务器中获取。
[3] 单独分离第三方库、自定义公共模块、webpack运行文件,它们各自在不同文件中
第一步:抽离 webpack 运行文件
修改 webpack.config.js 配置文件
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor', 'runtime'], // runtime为抽离的webpack运行文件的名字,名字是固定的
filename: '[name].js'
})
]
上面这段代码等价于下面这段代码:
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: '[name].js'
}),
new webpack.optimize.CommonsChunkPlugin({ // 用于抽离webpack运行文件
name: 'runtime',
filename: '[name].js',
chunks: ['vendor'] // 从哪里抽离,即"source chunks"是谁
})
]
这段抽离 webpack 运行文件的代码的意思是:创建一个名为 runtime 的 commons chunk 进行 webpack 运行文件的抽离,其中source chunks
是 vendor.js。
第二步:抽离第三方库和自定义公共模块
从第三方库中分离自定义公共模块,必须定义minChunks
属性才能成功抽离。minChunks 可以设置为数字、函数和 Infinity
,默认值是数字2(官方文档说默认值为入口文件的数量)。
minChunks取值:
- 数字:模块被多少个 chunk 公共引用才被抽取出来成为
commons chunk
; - 函数:接受(module, count)两个参数,返回一个布尔值,可以在函数内进行规定好的逻辑来决定某个模块是否提取成为
commons chunk
; - Infinity:只有当入口文件(entry chunks)大于 3 时才生效,用来从第三方库中分离自定义的公共模块。
修改 webpack.config.js 配置文件,要把第三方库和自定义公共模块分别单独抽离出来,首先需要将minChunks
属性设置为Infinity
。
const path = require('path');
const webpack = require('webpack'); // 导入webpack运行文件
const packagejson = require('./package.json'); // 导入项目package.json文件
module.exports = {
entry: { // 多入口文件的配置
"main1": "./src/main1.js",
"main2": "./src/main2.js",
"vendor": Object.keys(packagejson.dependencies) // 获取生产环境依赖的库
},
output: {
path: path.resolve('./dist'),
filename: "[name].js" // 对应多入口的多出口配置
},
watch: true,
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor', 'runtime'],
filename: '[name].js',
minChunks: Infinity // 设置minChunks属性
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
filename: '[name].js',
chunks: ['main1.js', 'main2.js'] // 从哪些文件中抽取commons chunk
})
]
};
此时 vendor.js、第三方文件、自定义公共模块、webpack 运行文件就抽离出来,并分别在不同文件中。
9. webpack.ensure 异步加载
webpack.ensure
有人称为异步加载,也有人叫它代码切割。其实就是把 JS 模块独立导出到一个.js
文件,然后在使用这个模块的时候,webpack 会构造script dom
元素,由浏览器发起异步请求获取这个.js
文件。
webpack.ensure 的原理:
把一些 JS 模块独立成一个个.js
文件,然后需要用到的时候,再创建一个script
对象,加入到document.head
对象中。浏览器会自动发起请求,去请求这个.js
文件,再通过回调函数,去定义得到这个.js
文件后,需要执行什么业务逻辑操作。
示例背景说明
main.js依赖三个js文件:(1) A.js是封装aBtn按钮点击后才执行的业务逻辑;
(2) B.js是封装bBtn按钮点击后才执行的业务逻辑;
(3) vue.js是封装了main.js需要利用的包。
A.js和B.js都不是main.js必须的,都是未来才可能发生的操作,那么可以利用异步加载,当发生的时候再去加载。
vue.js是main.js立即依赖的工具箱,但它又非常大,所以将其配置打包成一个公共模块,利用浏览器的并发加载,加快下载速度。
index.html 文件:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webpack的使用</title>
</head>
<body>
<div id="app"></div>
<button id="aBtn">A-btn</button>
<br>
<button id="bBtn">B-btn</button>
</body>
</html>
main.js 文件
// ECMAScript6的模块导入
import Vue from "vue"
console.log(Vue);
document.getElementById('aBtn').onclick = function () {
// 异步的加载A.js
require.ensure([], function () {
var A = require("./A.js");
alert(A.data);
})
};
document.getElementById('bBtn').onclick = function () {
// 异步的加载B.js
require.ensure([], function () { // ensure函数的第一个参数(数组[])用于添加回调函数中异步加载的JS文件的依赖文件的路径
var B = require("./B.js");
alert(B.data);
})
};
A.js & B.js
// A.js
var A = {
"data": "Hello A"
};
module.exports = A;
// B.js
var B = {
"data": "Hello B"
};
module.exports = B;
webpack 配置文件
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 导入webpack运行文件
const packagejson = require('./package.json'); // 导入项目package.json文件
module.exports = {
entry: {
"main": "./src/main.js",
"util": Object.keys(packagejson.dependencies)
},
output: {
path: path.resolve('./dist'),
filename: "[name].js" // 对应多入口的多出口配置
},
watch: true,
plugins: [
new webpack.optimize.CommonChunkPlugin({
name: "common",
filename: "[name].js"
}),
new HtmlWebpackPlugin({
// 主要用于多入口文件,当有多个入口文件的时候,它就会编译生成多个打包后的文件,chunks就能选择你要使用哪些JS文件
chunks: ["common", "util", "main"],
template: "./src/index.html",
inject: true // inject有四个值 true、body、head
})
]
};
五、v-for 中 key 的作用
1. 当数据发生变化时,vue是怎样更新节点的?
渲染真实DOM的开销是很大的。所以,我们先根据真实 DOM 生成一棵virtual DOM
,当virtual DOM
某个节点的数据改变后会生成一个新的Vnode
。然后Vnode
和oldVnode
作对比,发现有不一样的地方就直接修改在真实的DOM上,然后更新oldVnode
的值为Vnode
。这个过程就是diff算法:调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。
深度剖析 如何实现一个 Virtual DOM 算法
所谓的 Virtual DOM 算法,包括几个步骤:
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
- 当状态变更的时候,重新构建一棵新的对象树,然后用新的树和旧的树进行比较,记录两棵树的差异
- 把2中所记录的差异应用到步骤1所构建的真正的 DOM 树中,视图就更新了
2. vue-for 中 key 的作用
在使用v-for
循环遍历时,我们需要使用 key 来给每个节点做一个唯一的标识,Diff算法就可以正确的识别此节点,找到正确的位置插入新的节点。总之一句话,key 的作用主要是为了高效的更新虚拟 DOM。另外 vue 在使用相同标签元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。
参考链接:https://github.com/livoras/blog/issues/13
v-for
使用 key 的示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>v-for中的key</title>
<style>
.userList{
border: 1px solid red;
margin: 20px 0;
padding: 10px 10px;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="node_modules/vue/dist/vue.js"></script>
<script src="lodash.js"></script>
<script>
Vue.component('my-com', {
template: `
<div class="userList">
<div class="content">
<h3>{{obj.name}}</h3>
<p>{{obj.content}}</p>
</div>
<div id="control">
<input type="text" placeholder="请输入你的名字">
</div>
</div>
`,
props: {
obj: Object
}
});
var App = {
data(){
return {
datas: [
{id: 1, name: "张三", content: "我是张三"},
{id: 2, name: "李四", content: "我是李四"},
{id: 3, name: "王五", content: "我是王五"},
],
count: 0
}
},
template: `
<div>
<button @click="change">改变顺序</button>
<my-com v-for="(item, index) in datas" :obj="item" :key="item.id"></my-com>
</div>
`,
methods: {
change(){
console.log(this.count++);
this.datas = _.shuffle(this.datas);
}
}
};
new Vue({
el: "#app",
components: {
App
},
template: "<App/>"
})
</script>
</body>
</html>
如果不加 key ,点击“改变顺序”按钮,输入框的内容会和其相应的主题错乱开来。
总结一句话:当组件使用 v-for 遍历的时候,一定要绑定:key
属性,避免 vue 去计算 DOM。