搭建vue项目完整流程详解 2019.4
前言
这个教程从零开始一步一步说明搭建一个完整实用vue项目的所有流程,vue-cli + webpack + vue-router + element + axios,本人学习时间也不长,所以偏向使用级,如果有哪里不对不详,或者大家在项目过程中遇到了其他问题,都可留言给我,我也希望尽可能完善它。
一、准备工作
在用Vue.js构建大型应用的时候推荐使用NPM安装方法,需要的东西:
- node.js环境(npm包管理器)
- cnpm/npm的淘宝镜像
- vue-cli 脚手架构建工具
- webapck
1、安装node
去node官网 https://nodejs.org/en/ 下载,一路下一步即可。一般我选择更多人用的稳定版:
vue-cli官方文档对Node 版本也有要求,必须参考:
Vue CLI 需要 Node.js 8.9 或更高版本 (推荐 8.11.0+)。
安装完成后,打开命令行(快捷键window+R,输入cmd回车),输入指令node -v
查看node的版本,若出现相应的版本号,说明安装成功:
2、安装npm
其实npm包管理器是集成在node中的,在Node.js安装时就顺带装好了。可用命令行npm -v
查看版本:
但注意这未必是最新的版本,更新npm版本指令(三选一):
npm -g install npm@2.9.1 (安装固定某个版本,举个例子,不用跟着这个版本号)
npm install npm -g (g即全局;install是安装,可缩写成i,so,这个命令也可以写作 npm i npm -g)
npm update -g
3、补充说明cnpm
由于有些npm资源被屏蔽或者是国外资源,经常会导致npm安装依赖包失败,所以我们还需要npm的国内镜像——cnpm。
npm 默认的 registry 是 https://registry.npmjs.org ,下载 npm 包时是从国外的服务器下载,在国内很慢,一般将它设置为淘宝镜像: https://registry.npm.taobao.org。
使用cnpm的两种方式:
- 直接安装cnpm
指令为:npm install -g cnpm –registry=https://registry.npm.taobao.org
命令行cnpm -v
查看cnpm版本,检测是否安装成功:
这样做之后安装依赖的命令行都变成cnpm xxx,比如cnpm install vue-router。(当然你有了cnpm后也仍可用npm install从国外服务器下载)。 - 将npm的registry改成淘宝镜像
npm config set registry http://registry.npm.taobao.org/
然后npm get registry
或者npm config list
查看当前npm的镜像地址发生变化,如下图(原本应该是:https://registry.npmjs.org/):
这样做之后命令行仍用npm,但实质上是从国内的淘宝镜像下载,换心不换皮,挂羊头卖狗肉。
PS:将镜像改回原来的:npm config set registry https://registry.npmjs.org/
4、安装vue-cli 脚手架构建工具(必须在全局中进行安装)
vue-cli官网: https://cli.vuejs.org/zh
vue-cli用于快速搭建大型单页应用,可创建并启动一个带热重载、保存时静态检查以及可用于生产环境的构建配置的项目。
在官方文档中有说明:
关于旧版本
Vue CLI 的包名称由 vue-cli 改成了 @vue/cli。 如果你已经全局安装了旧版本的 vue-cli(1.x 或 2.x),你需要先通过 npm uninstall vue-cli -g 或 yarn global remove vue-cli 卸载它。(此截取文档注明编辑时间为 2018-8-12 10:20:44)
安装命令: npm install -g @vue/cli
检查其版本是否正确: vue --version
5、webpack
注意 : 在安装vue-cli时,已经自带安装webpack
二、构建项目
1、进入项目目录下
两种途径:
- 在 cmd 中
f:
回车可直接进入F盘,cd
进入特定目录(workspace)下:
- 有git bash
进入到目录下,右键菜单里选择 “git bash here”
2、构建名字为my_test的项目
输入命令行 vue init webpack my_test
回车(名字自定义)。
之后程序会问一堆问题,仔细看下每个选项。进行下一个按回车,如果不认可默认内容就直接输入自己的内容;或者输入y/n;或向上向下切换选项。设置项如下:
- Project name :项目名称 。注意:这里不能使用大写,
- Project description:项目描述。
- Author:作者,如果你有配置git的作者,他会读取。
- vue的打包方式Runtime Only (runtime方式)和 Runtime+Compiler(standalone方式):二选一,默认是常用的Runtime+Compiler,不需要自己手写render。
Runtime Only更加轻量,但是缺点是不能够编译jsx,在vue源码中,无论是render函数,还是template最终都是编译成render函数进行渲染的。所以,使用Runtime Only有时需要自己手动配置webpack对模板语法进行编译。
对这两者的理解不是很深刻,我就选了常用的的compiler
- Install vue-router ? 是否安装vue的路由插件,当然需要。
- Use ESLint to lint your code? 是否用ESLint来限制你的代码错误和风格。建议不要,要求特别严格,一不小心就报错。
- setup unit tests with Karma + Mocha? 是否需要安装单元测试工具Karma+Mocha,看自己,我不用
- Setup e2e tests with Nightwatch? 是否安装e2e来进行用户行为模拟测试,看自己,我不用
选择使用npm或yarn进行安装模块,我选yes。
这里注意它的提示,should we run ‘npm install’ ……,在选择yes的时候,表示你让程序自动运行命令npm install,你自己就不用手动输入np install来安装依赖了。
这些都选完了,就开始构建项目了,耐心等待,完成后就有了项目文件夹:
文件里各种配置和文件结构齐全,因为程序已经自动npm install,所以也会有存放依赖的node_modules目录:
3、运行项目
首先要命令行cd进入这个目录下,然后 npm run dev
项目就开始跑:
等待出现项目正在运行在8080端口的提示,项目就算运行成功了:
浏览器会自动打开这个地址 localhost:8080(如果没有自动打开网页,就在浏览器手动输入该网址)就能看到下面这个类似“hello world”的网页了:
注意
- 运行过程中回报很多warn警告,不影响运行的情况下不必关心,只有报error时才需要你去解决。
- 如果没有(上面标黄色那步)允许程序自动运行‘npm install’,在run dev之前一定要手动npm install,这个install具体相关的东西会在4.3、Element-ui里讲到
三、运行-初识项目
上面说到项目已经成功运行,我使用的IDE是webstorm,用webstorm打开项目,在IDE里也有操作命令行的地方,这里直接是你当前项目的目录下,不用再cd进入,很方便,所以一般我都是在这里写命令行:
在IDE里打开这个项目,很清楚地看到项目目录,这是刚构建好的最原始的状态,只有原生的vue和vue-router,其余地像element-ui、axios都还木有(如下图,这里是有nodo_modules目录的,只是我设置将其隐藏了):
下面了解下在这个自动构建的项目中如何写页面、配置路由、写样式、请求协议。这里就不讲过多原理,使用级,一步一步让它变成你希望的样子(本人也是个使用级菜鸟,大家可以一起学习下es6、webpack、node)。
1、为什么是localhost:8080?
为什么是这个服务地址来运行这个项目?是在config/index.js文件(如下图)里进行配置的,这是项目配置文件:
- 8080是默认端口,你可以改成8081、8082、8083……随你。如果你此时你的8080端口在跑别的项目被占用,即使你这里没改还是8080,当npm run dev跑起来,项目会自动启用下一个没被占用的端口8081,不会跑不起来的,如下:
- 再说说localhost,就是你电脑本地的地址,你会发现,你在自己的电脑上玩很顺,但是如果你开发时产品或者ui看下效果,你给她localhost:8080这个地址她是无法访问的,在她的电脑上localhost是指她的家,她家里没跑这个项目。这个情况下,你可以将这个host的值改成你电脑的ip地址(如下图所示):
这样只要你的电脑里项目是运行状态,在你自己的电脑和在别人的电脑都可以通过下面这个地址来访问你的项目(马赛克的部分是ip):
2、为什么是npm run dev?
run就是运行,dev意指开发环境,为什么是dev不是def、development?这个‘dev’定义在在package.json文件的scrips对象中:
(貌似是在node还是wepack下,run的时候就直接找这个package.json文件的这里)。实质上是跑的dev对应的后面这段很长的命令。你这里定义的是dev就是npm run dev,你这里写的是def就是npm run def。
这个很长的命令是执行bulid/webpack.dev.conf.js文件的配置,看这个文件的代码就会知道就是在这规定main.js是入口文件,生成后的文件注入index.html文件中。
3、index.html
index.html这个传统的html文件就是入口文件.如果是单页面项目,那么你的项目里也就这么一个html文件,meta头、编码、title、css外链和js文件调用都在这。
当然既然在webpack的项目下,我们所有的js、css都以模块的形式引入(怎么引入下面说),而不像传统的那样在head或者body底部引入。
这个文件里什么都没有,只有个id=app的div块,这个#app块就是关键,记做①(如下图),我们的页面就在这个div块里。
此时我打开控制台,dom树里也有个#app的div结构,记做②如下图:
那是不是①的#app就是②的#app呢?……并不是,页面上的②的#app在哪?(见4)又是怎样被放在①中的?(见5)下面我们一个一个说明。
4、App.vue
②的#app就在src/App.vue里,如下图(同样用②标记), 改变红圈中的id名,页面查看元素中dom树的id名也会跟着改变,而且很明显,这个.vue文件里有页面上的logo图片,还有路由入口。
上面我们说项目里只有一个html页面,其他的所有页面都是.vue文件,都看做组件,这个App.vue就是根组件 。
5、main.js
清楚了页面在根组件App.vue里,那么根组件是怎么放进index.html的①中的?就靠main.js来牵线搭桥。src/main.js是入口js文件,看看它做了什么:
- 首先,引入vue模块
- 引入App.vue并命名为App
- 引入vue-router路由的配置文件(注意,不是vue-router本身)
- 然后创建vue实例,这是根实例,注册App根组件(components),最终把这个根实例挂载到index.html的#app上(el),
四、打包-项目初成
1、打包
在项目目录下输入命令进行打包:npm run build
2、为什么是npm run build?
这个‘build’和dev一样是定义在在package.json文件的scrips对象中,实质是执行build/build.js文件:
主要的操作是
- 自动创建dist目录,每次执行该命令都会自动清空dist目录重新导入,不需要手动删除其中的文件。
- copy static目录到dist目录(单纯复制)
- 根据webpack.prod.conf.js配置文件,对源码进行编译(主要是src目录下的代码),输出到dist目录
3、本地运行打包后的文件
打包在dist目录下后生成一个html文件和一个static文件夹,staic目录下存放整个项目的所有资源,包括css、js、图片、字体等。
双击index.html文件根本无法访问页面,我们这里借助http-server方法。
- 首先全局安装http-server:
npm install http-server -g
- 进入到项目的dist目录下输入指令
http-server
,回车就会启动服务(如下图)来运行这个目录下的index.html,在浏览器输入其中任意一个url,就能运行这个打包好的项目了
上面已经成功运行了helloWorld了,但实际项目不可能是个helloWorld,这也满足不了项目需求。比如我现在要做一个项目,侧导航有项目管理、人员管理、消息管理,其中项目管理又分项目一、项目二,人员管理分班级一、班级二。(这大概是一般后台管理系统的模式)这如何实现?下面我们就进一步改造它,使之成为实际可用的项目。
五、改造-页面
1、src
src文件夹,是码源目录、开发目录,基本上绝大多数工作都是在这里开展的。初始状态下,这个目录只有
App.vue、main.js两个文件和assets、components、router三个文件夹。
2、views
示例页面HelloWorld.vue是放在components文件夹下面的,但我会将之后开发过程中自己写真正的组件放在
这,比如弹窗、日历选择器、进度条。而页面文件,我会在src目录下新建个views文件夹来存放。
特别注意:在.vue文件的<template></template>
中只能包含一个<div>
,全部内容应该全部写入一个根<div>
里。也就是说<template>
只能有一个儿子,很多很多孙子,但不能有两个儿子,不然会报错!
六、改造-路由vue-router
vue-router官方文档:https://router.vuejs.org/zh/
页面有了,我们如何实现页面访问、跳转、传参呢,有请vue-router。
上面说main.js里引入过vue-router的配置文件(注意,不是vue-router插件本身),这个文件在src/router/index.js。此文件内做了路由相关的配置:注册路由、定义路由映射、创建路由实例、设置路由拦截等。
1、理解helloWorld示例的路由应用
- router/index.js
helloWorld示例也有使用路由,我们看看它怎么实现的?下面是src/router/index.js的代码:①②③⑦④⑤
- 引入vue和vue-router
- 将vue-router挂载在Vue上
- 引入helloworld组件
- new一个router实例,传入一个路由映射参数——routes数组,这个数组中每个对象都声明了一个映射(一个组件的访问路径、名称等)
- 然后把整个router实例export导出
- 在main.js引入这个配置文件时就获得了这个路由实例,并将这个实例挂载到Vue根实例上(如下)
- router-view
路由被挂载到根实例上,根实例又挂载在App.vue的#app上,所以,在#app里放置<router-view/>
(如下)。
<router-view/>
这个标签是vue-router的内部组件,是路由出口,路由匹配到的组件将渲染在这里。示例中的路由组件只有helloworld,而App.vue中,路由出口在logo图片下,也就是说helloworld组件中的内容会渲染在logo图下面:
2、路由规划(嵌套路由)
理解完示例后来说我们自己的路由,写自己的路由配置之前,注意下,我们要构建类似这样有header,有侧导航栏aside,有主页面main的项目,点击侧导航栏主页面main部分进行切换,所以aside和header不变,main是嵌套在这的(如下图所示)。所以我们这里要做嵌套路由。
-
首先和实例一样,在App.vue里光溜溜放个
<router-view/>
顶层出口。(那个logo图就不要了) -
其次要有个layout.vue来放公用的header和aside,以及给不通的main页面来留下路由出口
<router-view/>
,这个文件同样放在views里。
之所以不直接在App.vue里直接写header、aside这些,是因为你的项目除了上图中这种布局的页面之外,肯定会有不带header、aside的其他布局的页面,比如登陆、错误页。 -
而router/index.js我们这样配置:(注意图中的注释和圈出的部分)
相应的我们访问页面:
但是/pro1的component是project,并且还有他的父级component:
所以可以这样理解,当你的path匹配到路由对象:
那么这个对象的component会被渲染在相应的<router-view/>
,(如果它有父级,即它是children数组的一员)他的父级component也会渲染在相应的<router-view/>
,因为儿子只有一个父亲,知道儿子就能找到父亲。
但是反过来,如果匹配到的这个路由对象,它有children,children的路由不会被渲染,因为一个爸爸可以有很多个儿子,只知道父亲并不能确定要找的是他哪个儿子。 -
但一般情况下,我们点击‘项目管理’,也希望能展示他下面第一个子项‘项目一’(或者某个特定子项)的页面:
这时有两个方法可以达成:
- 第一种,在页面要跳转“项目管理”的地方的path(如下)设成项目一的path即‘/pro1’,不用管直接输入url的情况:
router.push({ path: '/pro1' })
或者
<router-link to="/pro1">项目管理</router-link>
- 第二种,你就想在url的path是“/projectManage”能跳到“项目管理”,那就在router/index.js的路由映射中加个重定向(如下图所示),这样,即使跳转path写的“/proManage”,或者url输入“/projectManage”,它自己会跑到项目一的页面的。
3、页面跳转传参
上面我们说名路由配置、路由出口主要是用直接输入url的跳转方式说明,但是项目中大多应该是点击跳转,比如点击侧导航跳转,那么这个怎么实现?怎么传参?
很简单,也有两个方式(这里回忆下上面说路由对象它起码这么几个属性:path路由地址,component路由对应组件,还有个name路由名称,所以一下的两种方式都有根据path跳转和根据name两种):
- 声明式导航
使用 router-link 组件来导航,通过传入to
属性指定链接。如下图,有path
、name
两种,这里要看注释划重点,并结合后面的文字说明(图后面跟着的几点很重要!):
注意:
-
to
的值是对象,那么属于vue的特性插值情况,用v-bind
指令,有冒号。 -
这里
path
和name
的值要和router/index.js配置里的值保持一致。 -
当用
path
来跳转时:- 传参只能用
query
(path和query是一对cp,如果换成params传参,params和path不是cp,不来电不工作)。 - 在目标页面通过
this.$route.query
来获得参数:
- 可以看到,用path方式跳转,是带查询参数,url地址也变成/pro2?id=haha&plan=private,如图:
- 传参只能用
-
当用
name
路由名称来跳转时:- 传参只能用
params
,在目标页面通过this.$route.params
来获得参数:
- 这种方式,url地址不带参数,从地址栏无法看到传参:
- 传参只能用
- 编程式导航
即在js里用router.push
来实现。同样有path
、name
两种,使用方法如下图,注意事项和上面声明式导航相同:
4、页面注册
关于路由的基本应用就差不多了,以后再增加页面,一定要在router/index.js里进行注册。做过小程序的人,应该有这种习惯。
七、package.json
这是构建项目之后我们之后不论是用less还是用element-ui等等都要安装插件, 推荐使用 npm 的方式安装,它能更好地和 webpack 打包工具配合使用。正式安装之前,我们必须先介绍下package.json这个文件。
构建项目最后我们提到一定要npm install,这个命令的工作是什么?就是根据package.json这个文件里的dependencies的内容进行安装依赖,这些依赖会被下载到node_modules目录下。
dependencies
就是你程序跑起来(包括开发和生产环境)需要的模块,没有这个模块你程序就会报错。因为在初始化的时候我们值安装了vue和vue-router,所以这里就只有这两个:
npm install node_module –save
自动更新dependencies字段值devDependencies
见命知意了,开发程序的时候需要的模块了,像一些进行单元测试之类的包。
npm install node_module –save-dev
自动更新devDependencies字段
我们想安装依赖,可以在package.json的这里写好模块名和 版本号,npm install之后就安装好了,还有一种方法就是直接运行npm install模块名,这样之后这个某块名会自动出现在package.json里。
八、style
- style目录
写前端都知道,一个项目会有些公用css,比如reset.css或者项目中可复用的页面。我再src目录下创建style目录来存放这些css文件:
- 公用css
全局的公用css就在app.vue里引入(将css当做一个模块引入,用的是相对路径),如下。而一些复用页面的公共样式就在各个页面的style部分以同样的方法引入即可。
- scoped
公用的css文件提出来,每个页面特有的自己的样式写在.vue文件最下面的<style>
标签内即可。这时我们会发现style标签内有个scoped,这是用来干嘛的?
scoped 表明这里写的css 样式只适用于该组件(这个.vue文件),可以限定样式的作用域。
比如在在整个项目里你有无数个input,但你只希望在project1.vue里的input的高度为80px,那么在有scoped的情况下,就这个样式只会在这个文件里生效,对于其他组件(.vue文件)不会有任何影响。
九、Less
1、less-loader
在webpack的项目中你会看到各种loader,当你系统学习webpack官方文档就会看到加载器这个大章节,这是中文文档地址:https://www.html.cn/doc/webpack2/loaders/ 。
webpack 可以使用 loader 来预处理文件。也就是我们用了less就必须有less-loader(sass同样也需要sass-loader),这个loader我现在还是没特别明白,建议之后自己去系统学下webpack的配置,等我学会了应该也会整理成笔记。
2、使用less-loader
- 第一步,安装less-loader
在项目目录下输入命令:npm install --save-dev less-loader less
进行安装,之后就会看到在package.json的"devDependencies"里增加了两个模块:
- 第二步,在webpack.base.conf.js文件里增加这段代码
{
test: /\.less$/,
loader: "style-loader!css-loader!less-loader",
}
像这样:
好了,这样两步操作之后,保证我们在项目中正常使用less语法了。
3、注意事项
上面几种问题,有部分情况是因为无法使用less语法,但也有一部分是其他问题,总结下使用时的注意事项(在less-loader正常工作的条件下):
- 在
<style>
标签处没有lang="less"
时引入.less文件,.less文件里至少要有一句less语法,不然报错。 - 在引入.less文件的
<style>
标签处要加lang="less"
,不然less文件里的less语法的样式不生效。 - 直接在组件文件的
<style>
里写less样式,<style>
标签也要加lang="less"
,不然报错, - 在组件文件的
<style>
有lang="less"
,但是实际上里面没有less语法的样式, 不会有任何问题
十、assets和static
1、assets
assets 资源目录,一般存放开发过程中自己写的静态资源(image, css, js等),上面已经说了我的css放在了自建的style目录下,所以assets里我主要放图片。
这里的资源会被wabpack构建,这句话怎么理解?
以图片来说,在 *.vue 组件中,所有模板和CSS都会被 vue-html-loader 及 css-loader 解析,并查找资源URL,图片不是 JavaScript,当被视为模块依赖时,需要使用 url-loader 和 file-loader处理它,处理的具体方法配置在build/webpack.base.conf.js里:
这段代码表示build打包时,png、jpg等文件用url-loader进行处理,给出了10000B的限制,即10k,小于10k的图片转成base64码形式,大于10k的文件,加上hash值重命名,不管之前这个图片存在哪里,打包后全都存在存在dist/static/img文件夹路径下。要特别说明的是,这种处理只针对于src目录下的图片文件。结合下面的例子来说明这种处理机制。
当图片放在assets里,引用时,在css里用相对路径:
在开发时,这张图我放在了src/assets里,用相对路径(貌似不能绝对路径),但是build打包后,这张图根据上面说的处理方法变成这样:
在打包后,图片都在static/img里,所以css文件里引用也响应地做了改变(如下),这些都是loader做好了工作,我们不用管。
而因为数据绑定,我们也可以在js里引用图片,这时,可以使用相对路径,也可以使用绝对路径
@/assets/login/login-logo.png
,这种写法是利用了webpack的resolve.alias特性,执行时会把路径引用中的@符号,转换为相对应的路径,这里指代src目录。具体配置在build/webpack.base.conf.js文件(如下)。里但是这种@/assets
的绝对路径在css里不能使用,@是用js来解析的。
2、static
static也是资源目录,网上说里面放一些第三方库的文件,但是我并不是很懂,对于什么情况用哪个目录也很模糊,我还是说下图片放在这个目录下的引用和打包,大家从两者的区别中自行体会两者的应用场景。
static目录下的文件打包的时候不会被编译,只是纯拷贝到了dist下面的static文件夹,直接引用。
我们在开发时这样相对路径、绝对路径引用,都不会有任何问题:
但打包后发现页面的图片都出不来,引用路径错误。(如果你发现没这个问题,请往下看)
通过webpack+vuecli默认打包的css、js等资源,路径都是绝对的。因为我们的图片路径都是经历过文件夹的,在本地引用图片是绝对路径,但打包后因为把配置的static文件夹当成了根路径,所以很多图片找不到都不显示。
解决方法很简单,在config/index.js,把下图中的assetsPublicPath: '/'
改成assetsPublicPath: './',
特别注意:在写这个博客的时候我又试了一次,很神奇,在我没有改之前,打包好的文件跑起来也没有任何问题,图片引用正常,这是为什么呢?
因为vue-cli改了这个‘bug’,所以你什么都不用做,你的配置里还是assetsPublicPath: '/'
,打包后依然正常。2018年还需要操心这个问题,但2019年3月这次做这个项目就没有这个问题了。具体哪个版本就没问题了我也不太清楚,有知道的可以说一下。
十一、改造-ElementUI
Element官方文档:http://element-cn.eleme.io/#/zh-CN/component/installation
七、八、九、十都是为这里做准备而说明的。现在页面有了,路由也有了,布局也有了,我们来看样式,现在我所知道的最流行的vue样式组件库就是element-ui,它的官网这样自我介绍:
“Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库”
1、安装
- 进入项目目录下:
npm i element-ui -S
安装完后,就会在package.json里看到element,表示安装成功:
- 引入完整的 Element
在main.js文件里写入一下代码(注意第二条,一个都不能少):
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
- 但这里特别特别注意:上面说过公用css在App.vue里引用,而App.vue作为跟组件在main.js里注册。我们在引入
element-ui/lib/theme-chalk/index.css
这个element-ui的样式一定要在引入App.vue前,确保App.vue里引用的公用cssindex.css
会覆盖ElementUI样式,像这样:
这样你就可以在自己的view里使用element-ui的标签元素、组件了,在这里我用了它的布局:
2、如果你想改变element-ui的样式,很坑
写前端会用很多插件,但是ui又有自己的设计,所以经常对已有的组件的样式重写覆盖。这里我们不可避免地对用到的element-ui的组件会有些样式修改。
很多组件的api文档提供了一些样式修改的入口,比如导航菜单组件就有:
但未必能满足我们全部的需求。举个例子,可能不够贴切,但可以说明问题。还是这个导航菜单组件,这里为了清楚地说明,我在每个标签元素上都定义了自己class name:‘my-**’,如下:
现在,我们希望每一个菜单的高度为80px,并且有个红色的border,我们这样写:
但我们看效果未能达到预期,“导航一”这一栏并没有变化:
我们看上面代码,‘导航一’
是个有子项的一级菜单,不同于‘导航二’
的结构,这一栏是一个<template>
标签,看过vue的知道这是个虚拟的容器。
看元素(下图所示).my-sbumenu
是包括‘选项1、2、3’
的导航一
的所有内容,但并不是‘导航一’
这一栏自己的,所以想在这个类下来定义‘导航一’这栏的样式也是无效的,在‘导航一’
的<template>
命名的.my-item
并没有被写在下图的任何一个class里:
‘导航一’
这一栏实际上是被包在一个div.el-submenu_title
里,而这个div和这个class都是我们的view里没有出现的,是element-ui通过底层js创建的,我们似乎无法給这个div添加一个我们自己的class名:
不过我们既然能知道这个div,那就直接通过.el-submenu__title
来找到它,修改样式好了:
很遗憾,这个也是无效的, 不是你的样式优先级不够高(加!important
也没用),你会发现在下图右边根本没有你的样式:
这就是我说的坑。element-ui修改默认原生样式修改时,一部分元素你的class名是能够到达你想让它去的位置,如上面生效的那部分.my-item
。
但有部分元素明显是底层js创建插入dom中的,你无法命名,只能查看到它原生的class名,这种元素的样式修改,就需要在全局修改,也就是说上面样式不生效的原因在于scope
这个作用域的限制。如果想让样式生效,就去掉scoped
即可。
但是不推荐这么做,如果在每个页面因为修改原生css而去掉作用域限制,也就是这里所有的css都会变成全局的,会污染全局样式。
3、如何修改element-ui原生的样式
推荐这么做,局部生效的就写在局部,不要去掉scoped让组件样式污染全局:
将对底层js生成元素样式修改的这部分代码写在全局的样式文件
里,比如common.css(或者index.css,全局公共样式的引入上面有讲),而且这种原生样式的修改如果你只希望在特定某个组件文件里生效而不是全局改变,可以用它的祖先选择器做限制,比如我这个只在侧边栏.aside
里,就用.aside
限制,这样就既不会污染全局,也可以特定修改:
!!这里要注意,全局样式之所以能全局生效,不是因为它写在一个单独的css文件里,也不是因为它的名字是common.css或者index.css,而是引入这个css的<style>
标签没有scoped
。如果你写了个全局的公共样式文件,然后在引入的地方傻傻地也加了scoped
(像下图这样),辣么就只能傻眼了(自己理解不深刻时干过这种蠢事……粘样式的时候把scoped一块带过来了,就一直找不到问题出在哪)
十二、改造-axios
文档地址 : http://www.axios-js.com/zh-cn/docs/
请求协议是不可或缺的,这里我们用axios,这是文档对它的介绍:
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
特性
从浏览器中创建 XMLHttpRequests
从 node.js 创建 http 请求
支持 Promise API
拦截请求和响应
转换请求数据和响应数据
取消请求
自动转换 JSON 数据
客户端支持防御 XSRF
简单说,axios就相当于vue中的ajax异步请求,之前我用的是vue-resource,但是vue-resource不再继续维护了,就转战axios。
1、安装
进入项目目录下: npm install axios
安装成功,package.json会出现axios
2、使用
- api目录
在src下新建api目录,在这里放和服务器请求相关的文件,在api下新建index.js封装axios底层请求:引入aixos,创建axios实例,然后在这个实例上添加请求拦截器和响应拦截器(可有可无)、封装post请求和get请求。
api/index.js代码如下:
import axios from 'axios'
//创建axios实例
const instance = axios.create({
baseURL: 'http://xxx.xxx.x.xxx:8080', // api的base_url
timeout: 10000, // 请求超时时间
})
//请求拦截器
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
//响应拦截器
instance.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
//post请求
const post = function (url, params) {
return new Promise((resolve, reject) => {
instance({
url: url,
method: 'post',
params: params
}).then(res=> {
resolve(res);
}).catch(error => {reject(error);
})
});
}
//get请求
const get = function (url, params) {
return new Promise((resolve, reject) => {
instance({
url: url,
method: 'get',
params: params
}).then(res=> {
resolve(res);
}).catch(error => {
reject(error);
})
});
}
//暴露post、get方法
export default {post,get}
- 引入
然后,在main.js里,引入上面的index.js(即暴露的两个方法),再将api方法绑定到vue全局
import api from './api/index';
Vue.prototype.$api = api;
如图所示:
- 调用
这样就不用每次在用post、get时之前都要引入axios了,可全局调用底层封装的post、get方法,如下图所示:
- 注意事项
- 在调用请求方法时,传入的url参数不是绝对地址,而是像
/login
这样的,而在api/index.js里创建axios时配置的baseURL
(像http://xxx.xxx.x.xxx:8080
这样)会加到url
的前面,拼成完整的请求地址http://xxx.xxx.x.xxx:8080/login
- 在开发时我犯过一个低级错误,记录下:
我上面的login请求的全地址应该是http://xxx.xxx.x.xxx:8080/login。但是在搭建过程中我查看控制台实际的请求地址却变成http://localhost:8080/login,似乎我创建axios实例时配置的baseURL没有生效(如下图):
仔细检查才发现是个低级错误。如下图所示,我在上面这个地方创建了名为instance
实例,baseURL
也是在instance
这个实例上配置的,但是我下面的post时却是调用axios原生对象上的方法axios.post
(如左边圈出的错误用法,因为当时是直接将api文档上post方法示例代码复制过来,正确应该是instance.post
(右边)。
3、跨域问题(反向代理)
- 问题
请求url正确后,发现有有新问题,报错405 Not Allowed
:
看控制台log,显然,本地前端页面请求服务器协议会有跨域问题:
跨域问题经常会遇到,服务器那边可以解决,但我们前端不能只指望服务器,万一人家就是让你这边自己弄,我们也得有解决方法,毕竟跨域只会在开发中存在,而部署正式就不存在跨域。 - 反向代理
这里我们用反向代理解决无法跨域请求问题,打开config/index.js文件,proxyTable原本是个空对象(如图所示):
我们在这里设置反向代理,增加这段代码,把baseURL移到这里:
proxyTable: {
'/api': {
target: 'http://xxx.xxx.x.xxx:8080', //axiso实例里的baseURL的值
changeOrigin: true,
pathRewrite: {
//这里理解成用‘/api’代替target里面的地址,组件中我们调接口时直接用/api代替
// 比如我要调用'http://xxx.xxx.x.xxx:8080/login',直接写‘/api/user/add’即可 代理后地址栏显示/
'^/api': '/'
}
}
},
如上面注释所说,我们在api/index.js里baseURL的地方改成’/api’(如下图),其他调用的地方照旧。但是注意,因为改了config里的代码,所以要重启(npm run dev)才可以。
下面看到login请求成功,而且得到了服务器响应,反向代理跨域请求成功。这里url虽然显示的是localhost,但实际是config里的那个http:xxx.xxx.x.xxx:8080。
- 注意
跨域请求是只有在开发阶段(npm run dev)才有的问题,因为前后端分离,前端本地不跑服务器,在开发时都是请求后端开发人员的服务器。当开发完成打包(npm run build)后,将打包文件部署正式或者测试服务器时,服务器代码和前端代码会部署在统一台电脑上(不了解服务器,至今接触过的项目都是部署在一起的),不存在跨域问题。
4、优化
现在虽然功能上跑通了,但代码却很别扭,因为开发时(npm run dev),api/index.js里的baseURL由于跨域反向代理,要写成‘/api/’;而部署生产环境时(npm run build),baseURL又要改成测试服务器地址。
这样api/index.js里的baseURL的值手动改来改去很不智能,也很容易出错,很不好。我们的目标是webpack实现开发、生产环境的打包切换。
在/config/
目录下有prod.env.js
、dev.env.js
分别是生产环境、开发环境配置。
在任何文件里都能简单的用process.env
获取到当前的环境配置(不用任何文件引入):
所以api/index.js
里的baseURL
在开发环境和生产环境的值,分别放在各自配置文件的的BASE_URL里:
而api/index.js
里的baseURL
则换成当前环境变量的process.env.BASE_URL
:
这样配置后,当npm run dev
时:
当npm run build
后,再请求,url里没有‘/api/’:
上面这张图是我把打包好的文件扔到测试服务器,用测试地址
打的log。
但如果是http-server
本地运行打包文件,在控制台headers里看到的url是htttp://localhost:8080/login这种,不过这时url里只要没有/api/
,能够请求到数据,就说明配置达到了预期效果。
这里留一嘴,开发环境和生产环境区分开了,但是实际工作中,生产环境也分测试环境、正式环境,或者同一套代码套用在不通的项目上,这样url和一些title都不一样,都需要区分配置。这个怎么实现?之后我整理好了再发
5、关于token和拦截器
其实我也说不清token干嘛的,只知道服务器在登录协议会返个token,之后每个协议都要在header带上token,服务器会校验,如果token过期了需要重新登录。
-
拦截器
拦截器有请求拦截器和响应拦截器。会在每一个请求或响应被 then 或 catch 处理前拦截它们,并在这里做一些统一处理。在上面的api/index.js的代码中能看到拦截器。 -
token
我的token就在拦截器中处理。处理逻辑是:
- 首先在响应拦截器里判断这个协议返回值是否有token,有的话把它存在sessionStorage里;
- 其次在请求拦截器里在每个协议发送前将token塞进headers里;
- 最后在响应拦截器判断这个协议返回值的msg是否通知token过期(),如果过期,弹提示框给用户,并跳转登录
- 步骤
- 首先,在api/index.js里的代码的基础上引入Vue、router。
- 因为我们要弹element的提示框,上面讲在main.js引入Element的时候已经将Element挂载Vue上了
Vue.use(ElementUI)
,所以可以在引入Vue的条件下用Vue.prototype.$message()
调用提示框。 - 引用router是为了跳转。
- 因为我们要弹element的提示框,上面讲在main.js引入Element的时候已经将Element挂载Vue上了
import Vue from 'vue'
import router from '../router/index'
- 第二步,在响应拦截器中写入以下代码,有注释(其中响应的数据结构跟着各自的情况来,这里是用我自己的数据结构)
//响应拦截器
instance.interceptors.response.use(function (response) {
// 对响应数据做点什么
const res = response.data;
//token存sessionStorage
if(res.hasOwnProperty('token')){
sessionStorage.setItem('token',JSON.stringify({data:res.token}));
}
//我的协议这类错误提示为 msg: "token不存在或者无效"
if(!res.success && res.msg == 'token不存在或者无效'){
Vue.prototype.$message({
message: "token失效,请重新登录!",
type: 'warning',
})
router.push({path: '/'})
}else{
//如果没问题一定要返回
return res;
}
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
- 第三步,在请求拦截器
// request拦截器
instance.interceptors.request.use(
config => {
// 根据各自情况加入token-安全携带,我这每一个都要token,所以都是true
if (true) {
// 让每个请求携带token
config.headers['token'] = JSON.parse(sessionStorage.getItem('token'));
}
//一定要返回
return config;
},
error => {
// 请求错误处理
Promise.reject(error);
}
)
十三、改造-qs
要说qs以及为什么要用qs,就要先说说headers。
1、HTTP消息头
HTTP消息头包括:
- 通用信息头General
- 请求头Response Headers
- 响应头Request Headers
- 实体头Form Data /Query String Parameters/Request Payload
- 扩展头
我参考了参考:https://blog.csdn.net/j080624/article/details/56006705 这个博文,下面说说要用到的(标记加粗的一定要看),其他的自己找资料深入了解吧。
- 通用信息头General
即能用于请求消息中,也能用于响应信息中,但与被传输的实体内容没有关系的信息头
key | 含义 |
---|---|
Request URL | 表示请求的地址(网络资源位置) |
Request Method | 表情请求的方法类型get , post |
Status Code | 响应状态码: 200 - OK 成功返回响应 ; 301 - 资源(网页等)被永久转移到其它URL; 404 - 请求的资源(网页等)不存在; 500 - 内部服务器错误 |
Remote Address | 表示远程服务器地址,就是你本地的地址 |
Referrer Policy | 当从一个链接跳到另一个链接,另一个链接的referer就记录了是从哪个链接跳来的。 Referrer Policy就是管理这个来源信息的机制。 |
- 响应头Response Headers
key | 含义 |
---|---|
content-length | 响应体的长度(响应内容的字节数) |
content-type | 返回的内容类型,服务端发送的类型及采用的编码方式。 返回的响应MIME类型与编码–告诉浏览器它发送的数据属于什么文件类型。 |
date | 原始服务器消息发出的时间客户端请求服务端的时间 |
expires | 响应过期的日期和时间 |
Pragma | Pragma Pragma:no-cache ——服务端禁止客户端缓存页面数据 |
- 请求头Request Headers
key | 含义 |
---|---|
Accept | 客户端能接收的MIME 类型。也可以称为媒体类型和内容类型。application/json, text/plain, */* 斜杠前面的是 type(类型),斜杠后面的是 subtype(子类型)。 type 指定包含的时何种数据,subtype 标识数据的特定类型,即大类中的小类。 例如,image/jpeg,类型是image,子类型是jpeg。 已经定义了8个顶级类型: text/:用于标准化地表示的文本信息,文本消息可以是多种字符集和或者多种格式的; image/:表示图片; model/:表示3D模型,如VRML文件; audio/:表示声音; video/:表示视频,可能包括声音; message/:表示协议特定的信封,如email消息和HTTP响应; application/:用于传输应用程序数据或者二进制数据; multipart/:表示多个文档和资源的容器; |
Accept-Language | Accept-Language表示浏览器所支持的语言类型。zh-cn表示简体中文;zh 表示中文; |
Connection | 表示是否需要持久连接 |
Content-Length | 请求体的长度(响应内容的字节数) |
Content-Type | 服务端发送的类型及采用的编码方式 |
Cookie | 客户端暂存服务端的信息 |
User-Agent 用户代理 简称 UA | 告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本它是一个特殊字符串头 使得服务器能够识别客户端使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。 |
- 实体头
实体头有以下几种:
- Query String Parameters
- Form Data
Form Data 请求头部的 Content-Type: application/x-www-form-urlencoded - Request Payload
Request Payload 请求头部的 Content-Type: application/json,并且请求正文是一个 json 格式的字符串
补充:POST请求有请求体,故与GET请求相比,请求头中多了Content-Length和Content-Type属性。
2、为什么要用qs
默认情况下,axios将JavaScript对象序列化为JSON。 要以应用程序/ x-www-form-urlencoded格式发送数据。也就是我们最常用的请求协议data发送一个json对象:
data:{
user:'admin',
password:123456
}
但是我这个登录协议服务器要求以form-data的格式,如果我们像上面那样是请求不到对的数据
如何使我们的请求数据(js对象形式)转换成form-data的格式,下面有两种方法,其中一种就用到qs。
3、qs(全称Query String)
可以使用qs库的Qs.stringify()
将 json 对象序列化为请求参数字符串 (URL的形式,如 {name:'dahuang',age: 11}
转化为 name=dahuang&age=11
)。
- 第一步,QS安装
在目录下,命令:npm install qs --save
!!!!先别安装,往下看!!!
这个步骤是我参考网上的一些教程的,但是同时我也看到一种说法:Qs是axios里面自带的,可以直接引入。
然后我没有像上面npm安装,我的package.json
里也没有qs的依赖,试了下直接引用,发现是完全ok的。所以按照我实际的流程,是没有这一步的。 - 第二步,引入qs,转化请求参数
在src/api/index.js
里引入qs
然后在我要用的post方法请求参数处使用stringify()
方法,如下图:
之后,我的请求参数确实变成了form data
的形式:
并且也从服务器获得成功的相应:
- 第三步,设置请求头里的
'Content-Type'
为'application/x-www-form-urlencoded'
有人要问了,上一步不是就成功获得数据了吗,为什么还有一步?
这一步也是我从网上的教程看到的,并且上面说过,我这个/login协议示例中header
的Content-Type
值需要是application/x-www-form-urlencoded
,而之前是axios默认的application/json
。那么貌似确实需要这一步设置,保持一致。
但事实上,在第二步完成后还没有设置header
的Content-Type
之前,看了下我的请求头,它就已经是:
从理论上说,qs只转换数据格式,没有做其他的工作。
但从现象上看,当请求正文(参数)被转换成form-data
的形式,请求头的Content-Type
也跟着变成相应的application/x-www-form-urlencoded
。
这是我没弄明白的地方,有知道的还请不吝赐教。所以实际上这一步我也没有做。
4、优化点
在我的项目里,除了/login的协议,其他的post协议都是json参数,粗暴地把post方法参数用qs转化,那其他的协议不就没法用这个post方法了么?所以最终,post方法我没动,而是新加了个postForm方法来满足需要form-data参数的协议:
十四、预留vuex
之后会学习整理下vuex