第6章 Vue项目化
本文节选自《Vue.js从入门到项目实战》,书籍详情:item.jd.com/12513015.ht…
从本章开始,笔者将介绍一些使用Vue生态中其他成员进行项目开发的内容。这些内容是整个Vue生态中十分主流且核心的部分,不仅需要同学们能够看懂,还需要同学们在实战中能够得心应手地去应用。当然,要想做到这些,需要同学们跟着教程进行操作和练习。
6.1 项目快速构建
当下潮流的做法一般采用前后端分离的方式进行Web架构,但同时也对前端开发环境的搭建提出了更高的要求。一个完整的前端开发环境应该具备预编译模板、注入依赖、合并压缩资源、分离开发和生产环境以及提供一个模拟的服务端环境等功能。 对于初学者来说,能够理解这些概念的定义和应用已经十分不易,好在Vue为我们提供了项目的快速构建工具——Vue Cli。
6.1.1 Vue Cli简介
想起以前和朋友杂谈技术时,一个做Ruby的家伙说“Ruby on Rails是一个非常强大的Scaffolding(脚手架),用它一个小时就可以写个博客网站”。笔者心中暗自一惊,回去查了下这个“Scaffolding”单词,译为“脚手架”,但久久不能理解是什么意思,也不知“Ruby on Rails”到底是何方神圣。
之后,偶然有次机会去学习一个“Ruby on Rails”的项目源码,才明白其意思,不过尔尔。 Vue Cli也是一个“脚手架”,使用它5分钟就可以搭建一个完整的Vue应用。Vue Cli是Vue官方提供的构建工具,可用于快速搭建一个带有热重载(在代码修改后不必刷新页面即可呈现修改后的效果)、lint代码语法检测以及构建生产版本等功能的单页应用。
上面的内容牵扯到很多概念,初次接触的同学也不必担心,没有必要对其刨根究底,一是往往它们作为一个名词,用于沟通而已,我们只需要理解其作用和用法;二是往往它们太过抽象,需要结合实例进行理解。 下面笔者将演示如何使用Vue Cli快速构建一个Vue项目。
6.1.2 使用Vue Cli构建项目
1.打开控制台,输入: cnpm install vue-cli -g 安装Vue Cli,尚未安装cnpm的同学可以输入: npm install cnpm -g --registry=https://registry.npm.taobao.org 安装国内淘宝镜像源的cnpm)。 在命令执行结束之后,输入: vue --version 。如果控制台打印出版本号,即表示安装成功。
2.在项目所要放置的文件目录下打开控制台,输入: vue init webpack my-project 初始化项目(此处的my-project为项目名称)。
3.在模板下载完成后,Vue Cli将引导我们进行项目配置,笔者的配置如图6.1所示。
图6.1 Vue Cli项目初始化配置
其中,“Set up unit tests”和“Setup e2e tests with Nightwatch”选择“No”,这部分内容与Vue没有直接关系,这里不予探讨。最后一项也选择“no”是因为npm的镜像源在国外,安装依赖的速度缓慢且容易出错,笔者建议使用cnpm安装依赖。 4.输入: cnpm install 安装项目依赖。 5.输入: npm start 构建项目的开发版本,并启动webpack-dev-server。 此时,在浏览器地址栏输入http://localhost:8080即可访问项目,项目页面如图6.2所示。
图6.2 Vue Cli项目初始页面
6.之后,另开一个控制台,输入: npm run build 构建项目的生产版本。 6.1.3 项目目录介绍 打开初始化后的项目目录,可以发现里面已经存在了一些文件和文件夹,如图6.3所示。
图6.3 Vue Cli项目初始目录
目录主要内容的说明如表6.1所示。 表6.1 Vue Cli项目初始目录 名称 说明 build 开发和生产版本的构建脚本 config 开发和生产版本的部分构建配置 dist 由npm run build生成;项目的生产版本;项目完成后,交付该文件夹即可 src 项目开发的关键资源目录和主要工作空间 static 静态资源(如使用JS赋值图片的src时,该图片资源应放在static下) .babelrc babel的配置文件(babel,下一代JS的预编译器) .eslintignore ESLint代码语法检测的配置文件(应忽略的语法格式) .eslintrc.js ESLint代码语法检测的配置文件(应规范的语法格式) .gitignore 应被Git版本控制工具忽略的文件 index.html 应被webpack注入资源的模板HTML文件 之后使用编辑器(笔者使用的是WebStorm)打开项目,查看src文件夹下的内容,目录如图6.4所示。
图6.4 src目录下的内容 其中,assets文件夹用于存放图片、音频、视频等资源;components文件夹用于存放我们开发的单文件组件;router/index.js用于配置项目的前端路由(用到了Vue Router);App.vue是Vue Cli为我们默认创建的项目的根组件;main.js则是webpack的入口文件。 下面,我们先来看一下App.vue、main.js中的内容。 App.vue中的代码如下:
这是一个单文件组件,包含HTML、JS和CSS三个部分。显然,Vue Cli采用关注点分离的开发方式,这种开发方式使得组件的内聚性更强,也更适合于组件化的开发。 script标签中的内容为Vue组件;template标签中的内容为组件的DOM结构;style标签中的内容为CSS样式表(在被赋予scoped属性之后,样式表的作用域仅限在当前组件中)。 export和import是ES 6语法中用于模块化管理的两个关键字,这里使用export导出Vue组件以供外部调用。 main.js中的代码如下: // The Vue build version to load with the import
command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router'
Vue.config.productionTip = false
/* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '' }) 这里使用import引入全局的Vue对象、App组件和Vue Router的配置。之后,创建了一个Vue实例,并将App组件和router注册到实例中。 注释/eslint-disable no-new/用于告诉eslint忽略此处对new关键字的检测。 在main.js中,实例的el选项绑定了id为app的DOM元素。可这个元素在哪里呢?似乎App组件中有个id为app的div元素,是这个吗? 下面来看一下根目录(my-project)下index.html中的代码:
my-project 由于App组件是被注册在实例中的(作为实例的子组件),那么App组件中的元素当然不可能作为实例的挂载元素。那么,实例最终是被挂载在index.html中的div元素上了吗?其实也不是。 Vue Cli会将所有编译整理好的资源路径注入到以index.html为模板的镜像中,被注入后的镜像即生产版本中项目的入口文件,也就是dist文件目录下的index.html,这里的元素才是实例最终被挂载的地方。 在src目录下,还有一个重要的文件——使用Vue Router配置的router/index.js。有关Vue Router的内容笔者将放到下一小节中进行讲述。6.2 前端路由
路由这个概念首先出现在后台。传统MVC架构的web开发,由后台设置路由规则,当用户发送请求时,后台根据设定的路由规则将数据渲染到模板中,并将模板返回给用户。因此,用户每进行一次请求就要刷新一次页面,十分影响交互体验。 ajax的出现则有效解决了这一问题。ajax(asynchronous javascript and xml),浏览器提供的一种技术方案,采用异步加载数据的方式以实现页面局部刷新,极大提升了用户体验。 而异步交互体验的更高版本就是SPA——单页应用,不仅页面交互无刷新,甚至页面跳转之间也可以无刷新,前端路由随之应运而生。
6.2.1 前端路由的简单实现
广义上的前端路由是由前端根据URL来分发视图,其实现有两个核心操作,一是需要监听浏览器地址的变化,二是需要动态加载视图。 笔者分别使用Vue和原生的JS来模拟其实现,并用到Node.js创建服务端文件,服务端文件app.js的代码如下: const http = require('http') // http模块 const fs = require('fs') // 文件处理模块 const hostName = '127.0.0.1' const port = 3000 const server = http.createServer(function (req, res) { // 创建http服务 let content = fs.readFileSync('index.html') // 读取文件 res.writeHead(200, { // 设置响应内容类型 'content-type': 'text/html;charset="utf-8"' }) res.write(content) // 返回index.html文件内容 res.end() }) server.listen(port, hostName, function () { // 启动服务监听 console.log(Server is running here: http://${hostName}:${port}
) }) 这段代码用到了Node的http和fs模块,用以创建一个可以返回index.html页面的服务。想要启动服务,首先要到Node官网下载安装Node客户端(推荐使用8.11.3长期稳定版),之后在文件所处目录下输入命令: node app.js // node + 文件名 当控制台显示“Server is running here:http://127.0.0.1:3000”时,即表示服务启动成功。 下面来看一下使用Vue实现前端路由的代码(index.html):
- Home
- About
当点击About链接时,页面如图6.6所示。
6.2.2 Vue中的前端路由
Vue Router是Vue.js官方提供的路由管理器,它与Vue.js的核心深度集成,且随着Vue.js版本更新而更新,致力于简化单页应用的构建。 Vue Router的功能十分丰富且强大,笔者无意于枚举一些抽象的概念,下面笔者将通过几个简单的示例(需要运行在服务端上)来演示一下它的用法。 1.基础路由
- Home
- About
Vue.use(Router)
export default new Router({ routes: [ { path: '/', name: 'HelloWorld', component: HelloWorld } ] }) 这里使用Vue.use安装Vue Router插件。 这里使用export返回路由规则。默认只有当路径为/时,渲染HelloWorld组件。 关于Vue Router的知识点有很多,笔者在这里并没有一一列举,而是介绍了其最常见的几种用法。实际上,掌握这些已经能在实战中应对大部分的情况了。想要深研的同学可以查阅官网文档,并欢迎随时邮件与笔者进行交流。
6.3 状态管理
对于小型应用来说,完全没有必要引入状态管理,因为这会带来更多的开发成本。然而当应用的复杂度逐渐升高,状态管理的重要性也越发重要起来。 对于组件化开发来说,大型应用的多个状态往往跨越多个组件和交互间。在多层嵌套的父子组件之间传递状态已经十分麻烦,而Vue更是没有为兄弟组件提供直接共享数据的办法。基于这个问题,许多框架提供了解决方案——使用全局的状态管理器,将所有分散的共享数据交由状态管理器保管,Vue也不例外。 Vue官网提供的状态管理器名为Vuex,本节将介绍有关Vuex的概念与用法。
6.3.1 对象引用
在了解Vuex之前,我们先来看一下对象引用的概念。 下面这两段代码将输出什么(先不要看答案,自己思考一下)? // 代码1 let state = { msg: 'welcome' } let copy = state state.hello = "world" console.log(Object.keys(copy)) // Object.keys用于获取对象的键名 // 代码2 let state = { msg: "welcome" } let copy = state state = { hello: "world" } console.log(Object.keys(copy)) 答案如下: //-> ["msg", "hello"] //-> ["msg"] 在代码1中,当state对象被定义时,浏览器会为其分配一个地址;当使用state赋值copy对象时,copy将引用state的地址;因此,当state改变时,copy也随之改变。 在代码2中,笔者在为copy引用state的地址之后,重新定义了state对象;此时,state将引用一个新的地址,而copy仍引用原来的地址,所以copy并无任何变化。 理解了这个概念,将对我们学习和使用Vuex大有裨益。
6.3.2 状态管理器Vuex
Vuex,用于管理分散在Vue各个组件中的数据。 每一个Vuex应用的核心都是一个store(仓库),你也可以理解它是一个“非凡的全局对象”。与普通的全局对象不同的是,基于Vue数据与视图绑定的特点,当store中的状态发生变化时,与之绑定的视图也会被重新渲染。 这是一个单向的过程,因为store中的状态不允许被直接修改。改变store中的状态的唯一途径就是显式地提交(commit)mutation,这可以让我们方便地跟踪每一个状态的变化。(在大型的复杂应用中,如果无法有效地跟踪到状态的变化,将会对理解和维护代码带来极大的困扰。假如你能很好地理解使用Vuex进行状态管理的缘由,你就应该尽力遵循“显式”的原则,即使你可以跳过这一过程)。 Vuex中有5个重要的概念:State、Getter、Mutation、Action、Module。 State用于维护所有应用层的状态,并确保应用只有唯一的数据源(SSOT, Single Source of Truth)。 State的用法如下: new Vuex.Store({ // 创建仓库 state: { count: 1 } }) 在组件中,我们可以直接使用store.getters.tenTimesCount,也可以先用mapGetters辅助函数将其映射下来,代码如下: import { mapGetters } from 'vuex' export default { computed: { ...mapGetters(['tenTimesCount']) // ...是ES6中的对象展开运算符 } } Mutation提供修改State状态的方法。 Mutation的用法如下: new Vuex.Store({ // 创建仓库 state: { count: 0 }, mutations: { addCount (state, num) { state.count += num || 1 } } }) 在组件中,我们可以直接使用store.commit来提交mutation,代码如下: methods: { addCount () { this.
store } } 也可以先用mapMutation辅助函数将其映射下来,代码如下: import { mapState, mapMutations } from 'vuex' export default { computed: { ...mapState(['count']) // ...是ES6中的对象展开运算符 }, methods: { ...mapMutations(['addCount']), ...mapMutations({ // 为mutation赋别名,注意冲突;不常用 increaseCount: 'addCount' }) } } Action类似于Mutation,不同在于: Action不能直接修改状态,只能通过提交mutation来修改。 Action可以包含异步操作。 Action的用法如下: new Vuex.Store({ // 创建仓库 state: { count: 0 }, mutations: { addCount (state, num) { state.count += num || 1 } }, actions: { // context具有和store实例相同的属性和方法 // 可以通过context获取state和getters中的值,或者提交mutation和分发其他的action addCountAsync (context, num) { setInterval(function () { if (context.state.count < 2000) { context.commit('addCount', num || 100) } }, num || 100) } } }) 在组件中,我们可以直接使用store.dispatch来分发action,代码如下: methods: { addCountAsync (num) { this.$store.dispatch('addCountAsync', num) } } 或者使用mapActions辅助函数先将其映射下来,代码如下: import { mapState, mapActions } from 'vuex' export default { computed: { ...mapState(['count']) // ...是ES6中的对象展开运算符 }, methods: { ...mapActions(['addCountAsync']), ...mapActions({ // 为action赋别名,注意冲突;不常用 increaseCountAsync: 'addCountAsync' }) } } 由于使用单一状态树,当项目的状态非常多时,store对象就会变得十分臃肿。因此,Vuex允许我们将store分割成模块(Module),每个模块拥有独立的State、Getter、Mutation和Action,模块之中还可以嵌套模块,每一级都有着相同的结构。 Module的用法如下: // 定义模块 const counter = { namespaced: true, // 定义为独立的命名空间 state: { count: 0 }, getters: { // 在模块中,计算方法还会具有rootState、rootGetters参数以获取根模块中的数据 tenTimesCount (state, getters, rootState, rootGetters) { console.log(state, getters, rootState, rootGetters) return state.count * 10 } }, mutations: { addCount (state, num) { state.count += num || 1 } }, actions: { // context具有和store实例相同的属性和方法 // 可以通过context获取state和getters中的值,或者提交mutation、分发action // 在模块中,context还会具有rootState和rootGetters属性以获取根模块中的数据 addCountAsync (context, num) { setInterval(function () { if (context.state.count < 2000) { context.commit('addCount', num || 100) } }, num || 100) } } } // 创建仓库 new Vuex.Store({ modules: { // 注册模块 counter } }) 在组件中,模块的使用方法如下: import { mapState, mapGetters, mapMutations, mapActions } from 'vuex' export default { computed: { // 辅助函数的第一个参数为模块的名称 ...mapState('counter', ['count']), ...mapGetters('counter', ['tenTimesCount']) }, methods: { ...mapMutations('counter', ['addCount']), ...mapActions('counter', ['addCountAsync']) } } 最后,结合Vuex用于管理分散在各个组件中的状态和追踪状态变更的初衷,笔者简单总结了一下这些概念。作为一个状态管理器,首先要有保管状态的容器——State;为了满足衍生数据和数据链的需求,从而有了Getter;为了可以“显式地”修改状态,所以需要Mutation;为了可以“异步地”修改状态(满足ajax等异步数据交互),所以需要Action;最后,如果应用有成百上千个状态,放在一起会显得十分庞杂,所以分模块管理(Module)也是必不可少的。 Vuex的用法如上,应该并不难于理解。那么如何将Vuex集成到项目中去呢?笔者将在下一小节中进行介绍。 6.3.3 在项目中使用Vuex
首先,我们打开之前构建好的项目my-project,在命令行中输入: cnpm install vuex --save-dev 安装插件。 之后,在src目录下创建store、store/index.js、store/modules、store/modules/counter.js,创建好的文件路径如图6.8所示。
其中,store是我们进行Vuex仓库开发的工作目录;store/index.js是仓库的输出文件;store/modules目录用于放置各个模块;store/modules/counter.js文件是一个加数器模块。 store/modules/counter.js中的代码如下:
export default { namespaced: true, // 定义为独立的命名空间 state: { count: 0 }, getters: { // 在模块中,计算方法还会具有rootState、rootGetters参数以获取根模块中的数据 tenTimesCount (state, getters, rootState, rootGetters) { console.log(state, getters, rootState, rootGetters) return state.count * 10 } }, mutations: { addCount (state, num) { state.count += num || 1 } }, actions: { // context具有和store实例相同的属性和方法 // 可以通过context获取state和getters中的值,或者提交mutation、分发action // 在模块中,context还会具有rootState和rootGetters属性以获取根模块中的数据 addCountAsync (context, num) { setInterval(function () { if (context.state.count < 2000) { context.commit('addCount', num || 100) } }, num || 100) } } } store/index.js中的代码如下: import Vue from 'vue' import Vuex from 'vuex' import counter from './modules/counter' // 引入加数器模块
Vue.use(Vuex) // 安装插件
export default new Vuex.Store({ // 实例化Vuex仓库 modules: { counter } }) 在这两个文件中,笔者实例化了一个Vuex仓库并构建了一个加数器模块。之后,我们要在Vue实例中引入这个仓库,这还需要修改两个文件:webpack的入口文件main.js和单组件文件components/HelloWorld.vue。 修改后的main.js的代码如下: import Vue from 'vue' import App from './App' import router from './router' // 引入router import store from './store' // 引入store
Vue.config.productionTip = false
/* eslint-disable no-new */ new Vue({ // Vue实例 el: '#app', router, // 注册router store, // 注册store components: { App }, template: '' }) 在这里,笔者将仓库注册到了Vue实例中。 修改后的components/HelloWorld.vue的代码如下:
count: {{ count }}
ten times: {{ tenTimesCount }}
add Count add Count2在这里,笔者将仓库中的状态与视图进行了绑定。 项目的初始页面如图6.9所示。
当点击“add Count2”按钮之后,页面如图6.10所示。
Vuex并不是Vue应用开发的必选项,在使用时,应先考虑项目的规模和特点,有选择地进行取舍,盲目地选用只会带来更多的开发成本。 Vuex为开发者提供了多种写法,不过笔者并不推荐过多的尝试和写法上的变换,毕竟保持一致的风格也是高质量代码的一种表现,除非这种变化是一种进步。
笔者所认同的是,对于花里胡哨做到心中有数,对于自己做到大道至简和从一而终。 本章所介绍的工具和插件均是由Vue官方提供并维护的,你可以选择用或不用,取决于实际情况。 到这里,概念性的章节就结束了。从下一章开始,我们将进入实战课程。