webpack + vue
开始之前
本文包含以下技术,文中尽量给与详细的描述,并且附上参考链接,读者可以深入学习:
1、webpack
2、Vue.js
3、npm
4、ES6语法
前言
在对着产品高举中指怒发心中之愤后,真正能够解决问题的是自身上的改变,有句话说的好:你虽然改变不了全世界,但是你有机会改变你自己。秉承着“不听老人言,吃亏在眼前”的优良作风,我还是决定玩火自焚。
问题所在
之前的项目总结为以下内容:
1、AMD模块规范开发,使用requirejs实现,使用rjs打包,最终导致的结果是,输出的项目臃肿,肿的就像一坨狗不理……不忍直视
2、使用gulp进行打包,这一点貌似没有可吐槽的地方,毕竟都是被grunt折磨过来的……
3、数据的渲染使用模板引擎,这就意味着你要手动管理DOM,这样,你的业务代码参杂着你的数据处理、DOM管理,满屏幕的毛线……
4、模块化不足,虽然使用require进行了模块管理,但是大部分业务逻辑还是充斥在一个文件里,这与最近流行的组件化概念冰火不容,拒绝落后……
5、诸如 扩展性 、 维护性 我想早已不言而喻,不需赘述,再述就真TM是累赘了。
新框架要解决的问题:
1、要使构建输出的项目像你邻家小妹妹一样、瘦的皮包骨。(也许是营养不良)
2、要实现真正的模块化、组件化的开发方式,真正去解决维护难、扩展难的问题。(从此不怕产品汪)
3、业务逻辑专注数据处理,手动管理DOM的年代就像……像什么呢?(毕竟成人用品也越来越自动化了)
4、等等…….(其实好处无需赘述,来,往下看)
为了达成以上目标,我们探讨一下解决方案:
1、老项目的构建输出为什么臃肿?
答:因为使用的是require的rjs进行构建打包的,了解rjs的都知道,它会把项目所有依赖都打包在一个文件里,如果项目中有很多页面依赖这个模块,那么rjs并不会把这个模块提取出来作为公共模块,所以就会有很多复制性的内容,所以项目自然臃肿。
解决方案:使用webpack配合相应的loader,来完成模块加载和构建的工作。
2、老项目为什么模块化的不足?
答:老项目的模块化,仅仅体现在js层面,解决了模块引用的问题,但在开发方式上,依然可以看做是过程式的,这样的结果就导致了项目的难扩展和难维护,让开发人员在与产品汪的对峙中,并不从容。
解决方案:Vue.js能够很好的解决组件化的问题,配合 Vue.js 官方提供的vue-loader 能够很好的结合webpack做组件化的开发架构。
3、如何避免手动管理DOM?
答:如果你在做数据展示这一块的开发工作,相信你一定体会颇深,发送http请求到服务端,拿到返回的数据后手动渲染DOM至页面,这是最原始的开发方式,无非再加一个模板引擎之类的,但最终还是避免不了手动渲染,如果页面逻辑复杂,比如给你来一个翻页的功能,再来一个筛选项,估计你会觉得世界并不那么美好。
解决方案:MVVM模式能够很好的解决这个问题,而Vue.js的核心也是MVVM。
webpack
你肯定听说过webpack,如果直接对你描述什么是webpack你可能感受不到他的好处,那么在这之前,我相信你肯定使用过gulp或者grunt,如果你没使用过也可以,至少你要听说过并且知道gulp和grunt是干什么的,假如这个你还不清楚,那么你并不是一个合格的前端开发人员,这篇文章也不适合你,你可以从基础的地方慢慢学起。
gulp和grunt对于每一个前端开发人员应该是不陌生的,它们为前端提供了自动化构建的能力,并且有自己的生态圈,有很多插件,使得我们告别刀耕火种的时代,但是它们并没有解决模块加载的问题,比如我们之前的项目是使用gulp构建的,但是模块化得工作还是要靠require和rjs来完成,而gulp除了完成一些其他任务之外,就变成了帮助我们免除手动执行命令的工具了,别无它用。
而webpack就不同了,webpack的哲学是一切皆是模块,无论是js/css/sass/img/coffeejs/ttf….等等,webpack可以使用自定义的loader去把一切资源当做模块加载,这样就解决了模块依赖的问题,同时,利用插件还可以对项目进行优化,由于模块的加载和项目的构建优化都是通过webpack一个”人“来解决的,所以模块的加载和项目的构建优化并不是无机分离的,而是有机的结合在一起的,是一个组合的过程,这使得webpack在这方面能够完成的更出色,这也是webpack的优势所在。
如果你看不懂上面的描述,没关系,你只需要知道一下几点:
1、过去使用require和rjs等进行模块加载的方式,可以替换为webpack提供的指定loader去完成,你也可以自己开发加载特定资源的loader。
2、过去使用gulp和grunt完成项目构建优化的方式,可以替换成webpack提供的插件和特定的配置去完成。
3、由于模块的加载和项目的构建优化有机的结合,所以webpack能够更好的完成这项工作
4、并不是说有了webpack就淘汰的gulp等,有些特定的任务,还是要使用gulp去自定义完成的。但是不保证webpack的未来发展趋势会怎么样。
最后,给大家分享一个官方的教程,这个教程的最开始有坑的地方,如果读者遇到了坑,可以在这里给我留言,我会为大家解答,不过总体来讲,这个教程适合入门,唯一不足的就是教程是英文的,英文的也不用怕,本人的英语没过四级,但是现在依然能够看得懂英文技术文章。教程链接:http://blog.madewithlove.be/post/webpack-your-bags/
Vue.js
Vue.js 是什么
Vue.js(读音 /vjuː/,类似于 view) 是一套构建用户界面的渐进式框架。与其他重量级框架不同的是,Vue 采用自底向上增量开发的设计。Vue 的核心库只关注视图层,它不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与单文件组件和 Vue 生态系统支持的库结合使用时,Vue 也完全能够为复杂的单页应用程序提供驱动。
如果你是有经验的前端开发者,想知道 Vue.js 与其它库/框架的区别,查看对比其它框架。
起步
官方指南假设你已有 HTML、CSS 和 JavaScript 中级前端知识。如果你刚开始学习前端开发,将框架作为你的第一步可能不是最好的主意——掌握好基础知识再来!之前有其他框架的使用经验对于学习 Vue.js 是有帮助的,但这不是必需的。
尝试 Vue.js 最简单的方法是使用 JSFiddle Hello World 例子。你可以在浏览器新标签页中打开它,跟着例子学习一些基础用法。或者你也可以创建一个 .html
文件,然后通过如下方式引入 Vue:
<script src="https://unpkg.com/vue"></script> |
你可以查看安装教程来了解其他安装 Vue 的选项。请注意我们不推荐新手直接使用vue-cli
,尤其是对 Node.js 构建工具不够了解的同学。
声明式渲染
Vue.js 的核心是一个允许采用简洁的模板语法来声明式的将数据渲染进 DOM:
<div id="app"> {{ message }} </div> |
var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }) |
Hello Vue!
我们已经生成了我们的第一个 Vue 应用!看起来这跟单单渲染一个字符串模板非常类似,但是 Vue 在背后做了大量工作。现在数据和 DOM 已经被绑定在一起,所有的元素都是响应式的。我们如何知道?打开你的浏览器的控制台(就在这个页面打开),并修改app.message
,你将看到上例相应地更新。
除了文本插值,我们还可以采用这样的方式绑定 DOM 元素属性:
<div id="app-2"> <span v-bind:title="message"> 鼠标悬停几秒钟查看此处动态绑定的提示信息! </span> </div> |
var app2 = new Vue({ el: '#app-2', data: { message: '页面加载于 ' + new Date() } }) |
鼠标悬停几秒钟查看此处动态绑定的提示信息!
这里我们遇到点新东西。你看到的 v-bind
属性被称为指令。指令带有前缀 v-
,以表示它们是 Vue 提供的特殊属性。可能你已经猜到了,它们会在渲染的 DOM 上应用特殊的响应式行为。简言之,这里该指令的作用是:“将这个元素节点的 title
属性和 Vue 实例的message
属性保持一致”。
再次打开浏览器的 JavaScript 控制台输入 app2.message = '新消息'
,就会再一次看到这个绑定了 title
属性的 HTML 已经进行了更新。
条件与循环
控制切换一个元素的显示也相当简单:
<div id="app-3"> <p v-if="seen">现在你看到我了</p> </div> |
var app3 = new Vue({ el: '#app-3', data: { seen: true } }) |
现在你看到我了
继续在控制台设置 app3.seen = false
,你会发现 “现在你看到我了” 消失了。
这个例子演示了我们不仅可以绑定 DOM 文本到数据,也可以绑定 DOM 结构到数据。而且,Vue 也提供一个强大的过渡效果系统,可以在 Vue 插入/更新/删除元素时自动应用过渡效果。
还有其它很多指令,每个都有特殊的功能。例如,v-for
指令可以绑定数组的数据来渲染一个项目列表:
<div id="app-4"> <ol> <li v-for="todo in todos"> {{ todo.text }} </li> </ol> </div> |
var app4 = new Vue({ el: '#app-4', data: { todos: [ { text: '学习 JavaScript' }, { text: '学习 Vue' }, { text: '整个牛项目' } ] } }) |
- 学习 JavaScript
- 学习 Vue
- 整个牛项目
在控制台里,输入 app4.todos.push({ text: '新项目' })
,你会发现列表中添加了一个新项。
处理用户输入
为了让用户和你的应用进行互动,我们可以用 v-on
指令绑定一个事件监听器,通过它调用我们 Vue 实例中定义的方法:
<div id="app-5"> <p>{{ message }}</p> <button v-on:click="reverseMessage">逆转消息</button> </div> |
var app5 = new Vue({ el: '#app-5', data: { message: 'Hello Vue.js!' }, methods: { reverseMessage: function () { this.message = this.message.split('').reverse().join('') } } }) |
Hello Vue.js!
逆转消息
注意在 reverseMessage
方法中,我们更新了应用的状态,但没有触碰 DOM——所有的 DOM 操作都由 Vue 来处理,你编写的代码不需要关注底层逻辑。
Vue 还提供了 v-model
指令,它能轻松实现表单输入和应用状态之间的双向绑定。
<div id="app-6"> <p>{{ message }}</p> <input v-model="message"> </div> |
var app6 = new Vue({ el: '#app-6', data: { message: 'Hello Vue!' } }) |
Hello Vue!
组件化应用构建
组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、自包含和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:
在 Vue 里,一个组件本质上是一个拥有预定义选项的一个 Vue 实例,在 Vue 中注册组件很简单:
// 定义名为 todo-item 的新组件 Vue.component('todo-item', { template: '<li>这是个待办项</li>' }) |
现在你可以用它构建另一个组件模板:
<ol> <!-- 创建一个 todo-item 组件的实例 --> <todo-item></todo-item> </ol> |
但是这样会为每个待办项渲染同样的文本,这看起来并不炫酷,我们应该能将数据从父作用域传到子组件。让我们来修改一下组件的定义,使之能够接受一个属性:
Vue.component('todo-item', { // todo-item 组件现在接受一个 // "prop",类似于一个自定义属性 // 这个属性名为 todo。 props: ['todo'], template: '<li>{{ todo.text }}</li>' }) |
现在,我们可以使用 v-bind
指令将待办项传到每一个重复的组件中:
<div id="app-7"> <ol> <!-- 现在我们为每个 todo-item 提供待办项对象 待办项对象是变量,即其内容可以是动态的。 我们也需要为每个组件提供一个“key”,晚些时候我们会做个解释。 --> <todo-item v-for="item in groceryList" v-bind:todo="item" v-bind:key="item.id"> </todo-item> </ol> </div> |
Vue.component('todo-item', { props: ['todo'], template: '<li>{{ todo.text }}</li>' })
var app7 = new Vue({ el: '#app-7', data: { groceryList: [ { id: 0, text: '蔬菜' }, { id: 1, text: '奶酪' }, { id: 2, text: '随便其他什么人吃的东西' } ] } }) |
- 蔬菜
- 奶酪
- 随便其他什么人吃的东西
这只是一个假设的例子,但是我们已经设法将应用分割成了两个更小的单元,子单元通过props
接口实现了与父单元很好的解耦。我们现在可以进一步为我们的 todo-item
组件实现更复杂的模板和逻辑的改进,而不会影响到父单元。
在一个大型应用中,有必要将整个应用程序划分为组件,以使开发可管理。在后续教程中我们将详述组件,不过这里有一个(假想的)使用了组件的应用模板是什么样的例子:
<div id="app"> <app-nav></app-nav> <app-view> <app-sidebar></app-sidebar> <app-content></app-content> </app-view> </div> |
与自定义元素的关系
你可能已经注意到 Vue 组件非常类似于自定义元素——它是 Web 组件规范的一部分,这是因为 Vue 的组件语法部分参考了该规范。例如 Vue 组件实现了 Slot API 与 is
特性。但是,还是有几个关键差别:
Web 组件规范仍然处于草案阶段,并且尚无浏览器原生实现。相比之下,Vue 组件不需要任何补丁,并且在所有支持的浏览器(IE9 及更高版本)之下表现一致。必要时,Vue 组件也可以包装于原生自定义元素之内。
Vue 组件提供了纯自定义元素所不具备的一些重要功能,最突出的是跨组件数据流,自定义事件通信以及构建工具集成。
npm
npm 的全称是 nodejs包管理,现在越来越多的项目(包)都可以通过npm来安装管理,nodejs是js运行在服务器端的平台,它使得js的能力进一步提高,我们还要使用nodejs配合 webpack 来完成热加载的功能。所以读者最好有nodejs的开发经验,如果有express的经验更好。
让我们一步一步从零搭建这个项目
首先新建一个目录,名为 myProject ,这是我们的项目目录。然后执行一些基本的步骤,比如 npm init 命令,在我们的项目中生成 package.json 文件,这几乎是必选的,因为我们的项目要有很多依赖,都是通过npm来管理的,而npm对于我们项目的管理,则是通过package.json文件:
1 npm init
执行npm init之后,会提示你填写一些项目的信息,一直回车默认就好了,或者直接执行 npm init -y 直接跳过询问步骤
新建一个简单的项目目录,如下:
接下来通过npm安装项目依赖项:
npm install webpack webpack-dev-server vue-loader vue-html-loader css-loader vue-style-loader vue-hot-reload-api babel-loader babel-core babel-plugin-transform-runtime babel-preset-es2015 [email protected] --save-devnpm install vue --save
这个时候,你的package.json文件看起来应该是这样的:
"devDependencies": {"babel-core": "^6.3.17",
"babel-loader": "^6.2.0",
"babel-plugin-transform-runtime": "^6.3.13",
"babel-preset-es2015": "^6.3.13",
"babel-runtime": "^5.8.34",
"css-loader": "^0.23.0",
"vue-hot-reload-api": "^1.2.2",
"vue-html-loader": "^1.0.0",
"vue-style-loader": "^1.0.0",
"vue-loader": "^7.2.0",
"webpack": "^1.12.9",
"webpack-dev-server": "^1.14.0"
},
"dependencies": {
"vue": "^1.0.13"
},
我们安装了 babel 一系列包,用来解析ES6语法,因为我们使用ES6来开发项目,如果你不了解ES6语法,建议你看一看阮老师的教程,然后我们安装了一些loader包,比如css-loader/vue-loader等等,因为webpack是使用这些指定的loader去加载指定的文件的。
另外我们还使用 npm install vue –save 命令安装了 vue ,这个就是我们要在项目中使用的vue.js,我们可以直接像开发nodejs应用一样,直接require(‘vue’);即可,而不需要通过script标签引入,这一点在开发中很爽。
安装完了依赖,编辑以下文件并保存到相应位置:
1、index.html文件:
1 <!DOCTYPE html>2 <html lang="zh">
3 <head>
4 <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, user-scalable=no">
5 <meta charset="utf-8">
6 <title>首页</title>
7 </head>
8 <body>
9 <!-- vue的组件以自定义标签的形式使用 -->
10 <favlist></favlist>
11 </body>
12 </html>
2、index.js文件:
1 import Vue from 'Vue'2 import Favlist from './components/Favlist'
3
4 new Vue({
5 el: 'body',
6 components: { Favlist }
7 })
3、在components目录下新建一个 Favlist.vue 文件,作为我们的第一个组件:
1 <template>2 <div v-for="n in 10">div</div>
3 </template>
4
5 <script>
6 export default {
7 data () {
8 return {
9 msg: 'Hello World!'
10 }
11 }
12 }
13 </script>
14
15 <style>
16 html{
17 background: red;
18 }
19 </style>
我们首先在index.html中使用了自定义标签(即组件),然后在index.js中引入了Vue和我们的Favlist.vue组件,Favlist.vue文件中,我们使用了基本的vue组件语法,最后,我们希望它运行起来,这个时候,我们就需要webpack了。
在项目目录下新建 build 目录,用来存放我们的构建相关的代码文件等,然后在build目录下新建 webpack.config.js 这是我们的webpack配置文件,webpack需要通过读取你的配置,进行相应的操作,类似于gulpfile.js或者gruntfile.js等。
webpack.config.js
1 // nodejs 中的path模块2 var path = require('path');
3
4 module.exports = {
5 // 入口文件,path.resolve()方法,可以结合我们给定的两个参数最后生成绝对路径,最终指向的就是我们的index.js文件
6 entry: path.resolve(__dirname, '../app/index/index.js'),
7 // 输出配置
8 output: {
9 // 输出路径是 myProject/output/static
10 path: path.resolve(__dirname, '../output/static'),
11 publicPath: 'static/',
12 filename: '[name].[hash].js',
13 chunkFilename: '[id].[chunkhash].js'
14 },
15 module: {
16
17 loaders: [
18 // 使用vue-loader 加载 .vue 结尾的文件
19 {
20 test: /\.vue$/,
21 loader: 'vue'
22 }
23 ]
24 }
25 }
入口文件是index.js文件,配置了相应输出,然后使用 vue-loader 去加载 .vue 结尾的文件,接下来我们就可以构建项目了,我们可以在命令行中执行:
1 webpack --display-modules --display-chunks --config build/webpack.config.js
通过webpack命令,并且通过 –config 选项指定了我们配置文件的位置是 ‘build/webpack.config.js’,并通过 –display-modules 和 –display-chunks 选项显示相应的信息。如果你执行上面的命令,可能得到下图的错误:
错误提示我们应该选择合适的loader去加载这个 ‘./app/index/index.js’ 这个文件,并且说不期望index.js文件中的标识符(Unexpected token),这是因为我们使用了ES6的语法 import 语句,所以我们要使用 babel-loader 去加载我们的js文件,在配置文件中添加一个loaders项目,如下:
1 // nodejs 中的path模块2 var path = require('path');
3
4 module.exports = {
5 // 入口文件,path.resolve()方法,可以结合我们给定的两个参数最后生成绝对路径,最终指向的就是我们的index.js文件
6 entry: path.resolve(__dirname, '../app/index/index.js'),
7 // 输出配置
8 output: {
9 // 输出路径是 myProject/output/static
10 path: path.resolve(__dirname, '../output/static'),
11 publicPath: 'static/',
12 filename: '[name].[hash].js',
13 chunkFilename: '[id].[chunkhash].js'
14 },
15 module: {
16
17 loaders: [
18 // 使用vue-loader 加载 .vue 结尾的文件
19 {
20 test: /\.vue$/,
21 loader: 'vue'
22 },
23 {
24 test: /\.js$/,
25 loader: 'babel?presets=es2015',
26 exclude: /node_modules/
27 }
28 ]
29 }
30 }
现在再运行构建命令 : ‘webpack –display-modules –display-chunks –config build/webpack.config.js’
sorry,不出意外,你应该得到如下错误:
它说没有发现 ‘./components/Favlist’ 模块,而我们明明有 ./components/Favlist.vue 文件,为什么它没发现呢?它瞎了?其实是这样的,当webpack试图去加载模块的时候,它默认是查找以 .js 结尾的文件的,它并不知道 .vue 结尾的文件是什么鬼玩意儿,所以我们要在配置文件中告诉webpack,遇到 .vue 结尾的也要去加载,添加 resolve 配置项,如下:
1 // nodejs 中的path模块2 var path = require('path');
3
4 module.exports = {
5 // 入口文件,path.resolve()方法,可以结合我们给定的两个参数最后生成绝对路径,最终指向的就是我们的index.js文件
6 entry: path.resolve(__dirname, '../app/index/index.js'),
7 // 输出配置
8 output: {
9 // 输出路径是 myProject/output/static
10 path: path.resolve(__dirname, '../output/static'),
11 publicPath: 'static/',
12 filename: '[name].[hash].js',
13 chunkFilename: '[id].[chunkhash].js'
14 },
15 resolve: {
16 extensions: ['', '.js', '.vue']
17 },
18 module: {
19
20 loaders: [
21 // 使用vue-loader 加载 .vue 结尾的文件
22 {
23 test: /\.vue$/,
24 loader: 'vue'
25 },
26 {
27 test: /\.js$/,
28 loader: 'babel?presets=es2015',
29 exclude: /node_modules/
30 }
31 ]
32 }
33 }
这样,当我们去加载 ‘./components/Favlist’ 这样的模块时,webpack首先会查找 ./components/Favlist.js 如果没有发现Favlist.js文件就会继续查找 Favlist.vue 文件,现在再次运行构建命令,我们成功了,这时我们会在我们的输出目录中看到一个js文件:
之所以会这样输出,是因为我们的 webpack.config.js 文件中的输出配置中指定了相应的输出信息,这个时候,我们修改 index.html ,将输出的js文件引入:
1 <!DOCTYPE html>2 <html lang="zh">
3 <head>
4 <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, user-scalable=no">
5 <meta charset="utf-8">
6 <title>首页</title>
7 </head>
8 <body>
9 <!-- vue的组件以自定义标签的形式使用 -->
10 <favlist></favlist>
11
12 <script src="../../output/static/main.ce853b65bcffc3b16328.js"></script>
13 </body>
14 </html>
那么问题来了,难道我们每次都要手动的引入输出的js文件吗?因为每次构建输出的js文件都带有 hash 值,如 main.ce853b65bcffc3b16328.js,就不能更智能一点吗?每次都自动写入?怎么会不可能,否则这东西还能火吗,要实现这个功能,我们就要使用webpack的插件了,html-webpack-plugin插件,这个插件可以创建html文件,并自动将依赖写入html文件中。
首先安装 html-webpack-plugin 插件:
1 npm i html-webpack-plugin --save-dev
然后在修改配置项:
1 // nodejs 中的path模块2 var path = require('path');
3 var HtmlWebpackPlugin = require('html-webpack-plugin')
4
5 module.exports = {
6 // 入口文件,path.resolve()方法,可以结合我们给定的两个参数最后生成绝对路径,最终指向的就是我们的index.js文件
7 entry: path.resolve(__dirname, '../app/index/index.js'),
8 // 输出配置
9 output: {
10 // 输出路径是 myProject/output/static
11 path: path.resolve(__dirname, '../output/static'),
12 publicPath: 'static/',
13 filename: '[name].[hash].js',
14 chunkFilename: '[id].[chunkhash].js'
15 },
16 resolve: {
17 extensions: ['', '.js', '.vue']
18 },
19 module: {
20
21 loaders: [
22 // 使用vue-loader 加载 .vue 结尾的文件
23 {
24 test: /\.vue$/,
25 loader: 'vue'
26 },
27 {
28 test: /\.js$/,
29 loader: 'babel?presets=es2015',
30 exclude: /node_modules/
31 }
32 ]
33 },
34 plugins: [
35 new HtmlWebpackPlugin({
36 filename: '../index.html',
37 template: path.resolve(__dirname, '../app/index/index.html'),
38 inject: true
39 })
40 ]
41 }
然后再次执行构建命令,成功之后,看你的输出目录,多出来一个index.html文件,双击它,代码正确执行,你可以打开这个文件查看一下,webpack自动帮我们引入了相应的文件。
问题继续来了,难道每次我们都要构建之后才能查看运行的代码吗?那岂不是很没有效率,别担心,webpack提供了几种方式,进行热加载,在开发模式中,我们使用这种方式来提高效率,这里要介绍的,是使用 webpack-dev-middleware中间件和webpack-hot-middleware中间件,首先安装两个中间件:
1 npm install webpack-dev-middleware webpack-hot-middleware --save-dev
另外,还要安装express,这是一个nodejs框架
1 npm install express --save-dev
在开始之前,我先简单介绍一下这两个中间件,之所以叫做中间件,是因为nodejs的一个叫做express的框架中有中间件的概念,而这两个包要作为express中间件使用,所以称它们为中间件,那么他们能干什么呢?
1、webpack-dev-middleware
我们之前所面临的问题是,如果我们的代码改动了,我们要想看到浏览器的变化,需要先对项目进行构建,然后才能查看效果,这样对于开发效率来讲,简直就是不可忍受的一件事,试想我仅仅修改一个背景颜色就要构建一下项目,这尼玛坑爹啊,好在有webpack-dev-middleware中间件,它是对webpack一个简单的包装,它可以通过连接服务器服务那些从webpack发射出来的文件,它有一下几点好处:
1、不会向硬盘写文件,而是在内存中,注意我们构建项目实际就是向硬盘写文件。
2、当文件改变的时候,这个中间件不会再服务旧的包,你可以直接帅新浏览器就能看到最新的效果,这样你就不必等待构建的时间,所见即所得。
下面我们在build目录中创建一个 dev-server.js 的文件,并写入一下内容:
1 // 引入必要的模块2 var express = require('express')
3 var webpack = require('webpack')
4 var config = require('./webpack.config')
5
6 // 创建一个express实例
7 var app = express()
8
9 // 调用webpack并把配置传递过去
10 var compiler = webpack(config)
11
12 // 使用 webpack-dev-middleware 中间件
13 var devMiddleware = require('webpack-dev-middleware')(compiler, {
14 publicPath: config.output.publicPath,
15 stats: {
16 colors: true,
17 chunks: false
18 }
19 })
20
21 // 注册中间件
22 app.use(devMiddleware)
23
24 // 监听 8888端口,开启服务器
25 app.listen(8888, function (err) {
26 if (err) {
27 console.log(err)
28 return
29 }
30 console.log('Listening at http://localhost:8888')
31 })
此时,我们在项目根目录运行下面的命令,开启服务:
1 node build/dev-server.js
如果看到下图所示,证明你的服务成功开启了:
接下来打开浏览器,输入:
1 http://localhost:8888/app/index/index.html
如果不出意外,你应该得到一个404,如下图:
我们要对我们的 webpack.config.js 配置文件做两处修改:
1、将 config.output.publicPath 修改为 ‘/‘:
1 output: {2 // 输出路径是 myProject/output/static
3 path: path.resolve(__dirname, '../output/static'),
4 publicPath: '/',
5 filename: '[name].[hash].js',
6 chunkFilename: '[id].[chunkhash].js'
7 },
2、将 plugins 中 HtmlWebpackPlugin 中的 filename 修改为 ‘app/index/index.html’
1 plugins: [2 new HtmlWebpackPlugin({
3 filename: 'app/index/index.html',
4 template: path.resolve(__dirname, '../app/index/index.html'),
5 inject: true
6 })
7 ]
重启服务,再刷新页面,如果看到如下界面,证明你成功了:
但是这样开发模式下的确是成功了,可是我们直接修改了 webpack.config.js 文件,这就意味着当我们执行 构建命令 的时候,配置变了,那么我们的构建也跟着变了,所以,一个好的方式是,不去修改webpack.config.js文件,我们在build目录下新建一个 webpack.dev.conf.js文件,意思是开发模式下要读取的配置文件,并写入一下内容:
1 var HtmlWebpackPlugin = require('html-webpack-plugin')2 var path = require('path');
3 // 引入基本配置
4 var config = require('./webpack.config');
5
6 config.output.publicPath = '/';
7
8 config.plugins = [
9 new HtmlWebpackPlugin({
10 filename: 'app/index/index.html',
11 template: path.resolve(__dirname, '../app/index/index.html'),
12 inject: true
13 })
14 ];
15
16 module.exports = config;
这样,我们在dev环境下的配置文件中覆盖了基本配置文件,我们只需要在dev-server.js中将
1 var config = require('./webpack.config')
修改为:
1 var config = require('./webpack.dev.conf')
即可,然后,重启服务,刷新浏览器,你应该得到同样的成功结果,而这一次当我们执行构建命令:
1 webpack --display-modules --display-chunks --config build/webpack.config.js
并不会影响构建输出,因为我们没有直接修改webpack.config.js文件。
现在我们已经使用 webpack-dev-middleware 搭建基本的开发环境了,但是我们并不满足,因为我们每次都要手动去刷新浏览器,所谓的热加载,意思就是说能够追踪我们代码的变化,并自动更新界面,甚至还能保留程序状态。要完成热加载,我们就需要使用另外一个中间件 webpack-hot-middleware
2、webpack-hot-middleware
webpack-hot-middleware 只配合 webpack-dev-middleware 使用,它能给你提供热加载。
它的使用很简单,总共分4步:
1、安装,我们上面已经安装过了
2、在 webpack.dev.conf.js 配置文件中添加三个插件,如下:
1 var HtmlWebpackPlugin = require('html-webpack-plugin')2 var path = require('path');
3 var webpack = require('webpack');
4 // 引入基本配置
5 var config = require('./webpack.config');
6
7 config.output.publicPath = '/';
8
9 config.plugins = [
10 // 添加三个插件
11 new webpack.optimize.OccurenceOrderPlugin(),
12 new webpack.HotModuleReplacementPlugin(),
13 new webpack.NoErrorsPlugin(),
14
15 new HtmlWebpackPlugin({
16 filename: 'app/index/index.html',
17 template: path.resolve(__dirname, '../app/index/index.html'),
18 inject: true
19 })
20 ];
21
22 module.exports = config;
3、在 webpack.config.js 文件中入口配置中添加 ‘webpack-hot-middleware/client’,如下:
1 entry: ['webpack-hot-middleware/client', path.resolve(__dirname, '../app/index/index.js')],
4、在 dev-server.js 文件中使用插件
1 // 引入必要的模块2 var express = require('express')
3 var webpack = require('webpack')
4 var config = require('./webpack.dev.conf')
5
6 // 创建一个express实例
7 var app = express()
8
9 // 调用webpack并把配置传递过去
10 var compiler = webpack(config)
11
12 // 使用 webpack-dev-middleware 中间件
13 var devMiddleware = require('webpack-dev-middleware')(compiler, {
14 publicPath: config.output.publicPath,
15 stats: {
16 colors: true,
17 chunks: false
18 }
19 })
20
21 // 使用 webpack-hot-middleware 中间件
22 var hotMiddleware = require('webpack-hot-middleware')(compiler)
23
24 // 注册中间件
25 app.use(devMiddleware)
26 // 注册中间件
27 app.use(hotMiddleware)
28
29 // 监听 8888端口,开启服务器
30 app.listen(8888, function (err) {
31 if (err) {
32 console.log(err)
33 return
34 }
35 console.log('Listening at http://localhost:8888')
36 })
ok,现在重启的服务,然后修改 Favlist.vue 中的页面背景颜色为 ‘#000’:
1 <style>2 html{
3 background: #000;
4 }
5 </style>
然后查看你的浏览器,是不是你还没有刷新就已经得带改变了?
那么这样就完美了吗?还没有,如果你细心,你会注意到,我们上面在第2步中修改了 webpack.config.js 这个基本配置文件,修改了入口配置,如下:
1 entry: ['webpack-hot-middleware/client', path.resolve(__dirname, '../app/index/index.js')],
这也会导致我们之前讨论过的问题,就是会影响构建,所以我们不要直接修改 webpack.config.js 文件,我们还是在 webpack.dev.conf.js 文件中配置,如下:
1 var HtmlWebpackPlugin = require('html-webpack-plugin')2 var path = require('path');
3 var webpack = require('webpack');
4 // 引入基本配置
5 var config = require('./webpack.config');
6
7 config.output.publicPath = '/';
8
9 config.plugins = [
10 new webpack.optimize.OccurenceOrderPlugin(),
11 new webpack.HotModuleReplacementPlugin(),
12 new webpack.NoErrorsPlugin(),
13 new HtmlWebpackPlugin({
14 filename: 'app/index/index.html',
15 template: path.resolve(__dirname, '../app/index/index.html'),
16 inject: true
17 })
18 ];
19
20 // 动态向入口配置中注入 webpack-hot-middleware/client
21 var devClient = 'webpack-hot-middleware/client';
22 Object.keys(config.entry).forEach(function (name, i) {
23 var extras = [devClient]
24 config.entry[name] = extras.concat(config.entry[name])
25 })
26
27 module.exports = config;
但是我们还是要讲 webpack.config.js 文件中的入口配置修改为多入口配置的方式,这个修改不会影响构建,所以无所谓:
1 entry: {2 index: path.resolve(__dirname, '../app/index/index.js')
3 },
重启你的服务,刷新一下浏览器,然后修改 Favlist.vue 中的背景色为 green:
1 <style>2 html{
3 background: green;
4 }
5 </style>
再次查看浏览器,发现可以热加载。但是这样就结束了吗?还没有,不信你修改 index.html 文件,看看会不会热加载,实际上不会,你还是需要手动刷新页面,为了能够当 index.html 文件的改动也能够触发自动刷新,我们还需要做一些工作。
第一步:在 dev-server.js 文件中监听html文件改变事件,修改后的 dev-server.js 文件如下:
1 // 引入必要的模块2 var express = require('express')
3 var webpack = require('webpack')
4 var config = require('./webpack.dev.conf')
5
6 // 创建一个express实例
7 var app = express()
8
9 // 调用webpack并把配置传递过去
10 var compiler = webpack(config)
11
12 // 使用 webpack-dev-middleware 中间件
13 var devMiddleware = require('webpack-dev-middleware')(compiler, {
14 publicPath: config.output.publicPath,
15 stats: {
16 colors: true,
17 chunks: false
18 }
19 })
20
21 var hotMiddleware = require('webpack-hot-middleware')(compiler)
22
23 // webpack插件,监听html文件改变事件
24 compiler.plugin('compilation', function (compilation) {
25 compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
26 // 发布事件
27 hotMiddleware.publish({ action: 'reload' })
28 cb()
29 })
30 })
31
32 // 注册中间件
33 app.use(devMiddleware)
34 // 注册中间件
35 app.use(hotMiddleware)
36
37 // 监听 8888端口,开启服务器
38 app.listen(8888, function (err) {
39 if (err) {
40 console.log(err)
41 return
42 }
43 console.log('Listening at http://localhost:8888')
44 })
45 从上面的代码中可以看到,我们增加了如下代码:
46
47 1
48 2
49 3
50 4
51 5
52 6
53 7
54 8
55 // w
从上面的代码中可以看到,我们增加了如下代码:
1 // webpack插件,监听html文件改变事件2 compiler.plugin('compilation', function (compilation) {
3 compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
4 // 发布事件
5 hotMiddleware.publish({ action: 'reload' })
6 cb()
7 })
8 })
这段代码可能你看不懂,因为这涉及到webpack插件的编写,读者可以参阅下面的连接:
webpack 插件doc1
webpack 插件doc2
在这段代码中,我们监听了 ‘html-webpack-plugin-after-emit’ 事件,那么这个事件是从哪里发射的呢?我们通过名字可知,这个事件应该和html-webpack-plugin这个插件有关,在npm搜索 html-webpack-plugin 插件,在页面最底部我们可以发现如下图:
我们可以看到,html-webpack-plugin 这个插件的确提供了几个可选的事件,下面也提供了使用方法,这样,我们就能够监听到html文件的变化,然后我们使用下面的代码发布一个事件:
1 hotMiddleware.publish({ action: 'reload' })
第二步:修改 webpack.dev.conf.js 文件如下:
1 var HtmlWebpackPlugin = require('html-webpack-plugin')2 var path = require('path');
3 var webpack = require('webpack');
4 // 引入基本配置
5 var config = require('./webpack.config');
6
7 config.output.publicPath = '/';
8
9 config.plugins = [
10 new webpack.optimize.OccurenceOrderPlugin(),
11 new webpack.HotModuleReplacementPlugin(),
12 new webpack.NoErrorsPlugin(),
13 new HtmlWebpackPlugin({
14 filename: 'app/index/index.html',
15 template: path.resolve(__dirname, '../app/index/index.html'),
16 inject: true
17 })
18 ];
19
20 // var devClient = 'webpack-hot-middleware/client';
21 var devClient = './build/dev-client';
22 Object.keys(config.entry).forEach(function (name, i) {
23 var extras = [devClient]
24 config.entry[name] = extras.concat(config.entry[name])
25 })
26
27 module.exports = config;
我们修改了devClient变量,将 ‘webpack-hot-middleware/client’ 替换成 ‘./build/dev-client’,最终会导致,我们入口配置会变成下面这样:
1 entry: {2 index: [
3 './build/dev-client',
4 path.resolve(__dirname, '../app/index/index.js')
5 ]
6 },
第三步:新建 build/dev-client.js 文件,并编辑如下内容:
1 var hotClient = require('webpack-hot-middleware/client')2
3 // 订阅事件,当 event.action === 'reload' 时执行页面刷新
4 hotClient.subscribe(function (event) {
5 if (event.action === 'reload') {
6 window.location.reload()
7 }
8 })
这里我们除了引入 ‘webpack-hot-middleware/client’ 之外订阅了一个事件,当 event.action === ‘reload’ 时触发,还记得我们在 dev-server.js 中发布的事件吗:
1 hotMiddleware.publish({ action: 'reload' })
这样,当我们的html文件改变后,就可以监听的到,最终会执行页面刷新,而不需要我们手动刷新,现在重启服务,去尝试能否对html文件热加载吧。答案是yes。
好了,开发环境终于搞定了,下面我们再来谈一谈生产环境,也就是构建输出,我们现在可以执行一下构建命令,看看输出的内容是什么,为了不必每次都要输入下面这条长命令:
1 webpack --display-modules --display-chunks --config build/webpack.config.js
我们在 package.js 文件中添加 “scripts” 项,如下图:
这样,我们就可以通过执行下面命令来进行构建,同时我们还增加了一条开启开发服务器的命令:
// 构建npm run build
// 开启开发服务器
npm run dev
回过头来,我们执行构建命令: npm run build,查看输出内容,如下图:
现在我们只有一个js文件输出了,并没有css文件输出,在生产环境,我们希望css文件生成单独的文件,所以我们要使用 extract-text-webpack-plugin 插件,安装:
1 npm install extract-text-webpack-plugin --save-dev
然后在build目录下新建 webpack.prod.conf.js 文件,顾名思义,这个使我们区别于开发环境,用于生产环境的配置文件,并编辑一下内容:
1 var HtmlWebpackPlugin = require('html-webpack-plugin')2 var ExtractTextPlugin = require('extract-text-webpack-plugin')
3 var path = require('path');
4 var webpack = require('webpack');
5 // 引入基本配置
6 var config = require('./webpack.config');
7
8 config.vue = {
9 loaders: {
10 css: ExtractTextPlugin.extract("css")
11 }
12 };
13
14 config.plugins = [
15 // 提取css为单文件
16 new ExtractTextPlugin("../[name].[contenthash].css"),
17
18 new HtmlWebpackPlugin({
19 filename: '../index.html',
20 template: path.resolve(__dirname, '../app/index/index.html'),
21 inject: true
22 })
23 ];
24
25 module.exports = config;
然后修改 package.json 文件中的 script 项为如下:
1 "scripts": {2 "build": "webpack --display-modules --display-chunks --config build/webpack.prod.conf.js",
3 "dev": "node ./build/dev-server.js"
4 },
我们使用 webpack.prod.conf.js 为配置去构建,接下来执行:
npm run build
查看你的输出内容,如下图,css文件未提取出来了:
另外我们还可以添加如下插件在我们的 webpack.prod.conf.js 文件中,作为生产环境使用:
1 config.plugins = [2 new webpack.DefinePlugin({
3 'process.env': {
4 NODE_ENV: '"production"'
5 }
6 }),
7 // 压缩代码
8 new webpack.optimize.UglifyJsPlugin({
9 compress: {
10 warnings: false
11 }
12 }),
13 new webpack.optimize.OccurenceOrderPlugin(),
14 // 提取css为单文件
15 new ExtractTextPlugin("../[name].[contenthash].css"),
16 new HtmlWebpackPlugin({
17 filename: '../index.html',
18 template: path.resolve(__dirname, '../app/index/index.html'),
19 inject: true
20 })
21 ];
大家可以搜索这些插件,了解他的作用,这篇文章要介绍的太多,所以我一一讲解了。
到这里实际上搭建的已经差不多了,唯一要做的就是完善,比如公共模块的提取,如何加载图片,对于第一个问题,如何提取公共模块,我们可以使用 CommonsChunkPlugin 插件,在 webpack.prod.conf.js 文件中添加如下插件:
1 new webpack.optimize.CommonsChunkPlugin({2 name: 'vendors',
3 filename: 'vendors.js',
4 }),
然后在 webpack.config.js 文件中配置入口文件:
1 entry: {2 index: path.resolve(__dirname, '../app/index/index.js'),
3 vendors: [
4 'Vue'
5 ]
6 },
上面代码的意思是,我们把Vue.js当做公共模块单独打包,你可以在这个数组中增加其他模块,一起作为公共模块打包成一个文件,我们执行构建命令,然后查看输出,如下图,成功提取:
对于加载图片的问题,我们知道,webpack的哲学是一切皆是模块,然后通过相应的loader去加载,所以加载图片,我们就需要使用到 url-loader,在webpack.config.js 文件中添加一个loader配置:
1 loaders: [2 // 使用vue-loader 加载 .vue 结尾的文件
3 {
4 test: /\.vue$/,
5 loader: 'vue'
6 },
7 {
8 test: /\.js$/,
9 loader: 'babel?presets=es2015',
10 exclude: /node_modules/
11 },
12 // 加载图片
13 {
14 test: /\.(png|jpg|gif|svg)$/,
15 loader: 'url',
16 query: {
17 limit: 10000,
18 name: '[name].[ext]?[hash:7]'
19 }
20 }
21 ]
这样,当我们的css样式表文件中使用 url(xxxx.png)或者js中去require(‘xxxx.png’)的时候,webpack就知道如何处理,另外url-loader的一个好处就是,以上面的配置来说,当我们的图片大小小于10000字节的时候,webpack会把图片转换成base64格式插入到代码中,从而减少http请求,另外,这里谈到的任何一个loader都可以在npm中查找到,可以查询更多的loader了解并使用。
转载请标明本文来源:http://www.cnblogs.com/yswenli/p/7218813.html
更多内容欢迎star作者的github:https://github.com/yswenli/
如果发现本文有什么问题和任何建议,也随时欢迎交流~
感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是yswenli 。
以上是 webpack + vue 的全部内容, 来源链接: utcz.com/z/375314.html