- 一.什么是库?什么是框架?
- 二.MVC 和 MVVM 区别
- 三.Vue的基本使用
- 四.指令的使用
- 五.自定义指令
- 六.
vue-cli
项目创建 - 七.你需要了解的包管理工具与配置项
- 八.Vue组件通信
- 九.学会编写可复用性模块
- 十.表单组件的封装
- 十一.作用域插槽
- 十二.项目实战
- 十三.
Vuex
配置 - 十四.页面使用如何请求
- 十五.路由钩子鉴权 permission.js
- 十六.扩充你的开发工具
- 十七项目上手前,需要怎么快速进入开发
- 文档参考
一.什么是库?什么是框架?
- 库是将代码集合成一个产品,库是我们调用库中的方法实现自己的功能。
- 框架则是为解决一类问题而开发的产品,框架是我们在指定的位置编写好代码,框架帮我们调用。
二.MVC 和 MVVM 区别
- 传统的 MVC 指的是,用户操作会请求服务端路由,路由会调用对应的控制器来处理,控制器会获取数 据。将结果返回给前端,页面重新渲染
- MVVM :传统的前端会将数据手动渲染到页面上, MVVM 模式不需要用户收到操作 dom 元素,将数据绑 定到 viewModel 层上,会自动将数据渲染到页面中,视图变化会通知 viewModel层 更新数据。 ViewModel 就是我们 MVVM 模式中的桥梁.
<template>
<div @click="add"></div>
</template>
<script>
export default {
name: "App",
data() {
return {
num: 0,
};
},
methods: {
add() {
this.num++;
},
},
};
</script>
Vue并没有完全遵循MVVM模型,严格的MVVM模式中,View层不能直接和Model层通信,只能通过ViewModel来进行通信。
三.Vue的基本使用
快速安装
1、如果项目使用,直接使用按照脚手架开发
2、如果刚开始接触vue的,可以用script标签引入。
简单的Vue中的模板
<script src="node_modules/vue/dist/vue.js"></script>
<!-- 3.外部模板 -->
<div id="app">{{name}}</div>
<script>
const vm = new Vue({
el:'#app',
data:{
name:'jw',
age: 22
},
// 2.内部模板
template:'<div>{{age}}</div>',
// 1.render函数
render(h){
return h('h1',['hello,',this.name,this.age])
}
});
</script>
我们默认使用的是
runtime-with-compiler
版本的vue,带compiler的版本才能使用template属性,内部会将template编译成render函数
- 渲染流程,会先查找用户传入的render
- 如果没有传入render则查找template属性
- 如果没有传入template则查找el属性,如果有el,则采用el的模板
模板语法
我们可以在vue中使用表达式语法,表达式会在所属 Vue 实例的数据作用域下作为 JavaScript 被解析。
<div id="app">
<!-- 可以放入运算的结果 -->
{{ 1+ 1 }}
<!-- 当前这个表达式 最后会被编译成函数 _v(msg === 'hello'? true:false) -->
{{msg === 'hello'? true:false}}
<!-- 取值操作,函数返回结果 -->
{{obj.a}} {{fn()}}
</div>
这里不能使用js语句(
var a = 1
),带有返回值的都可以应用在模板语法中。
响应式原则
- Vue内部会递归的去循环vue中的data属性,会给每个属性都增加getter和setter,当属性值变化时会更新视图。
- 重写了数组中的方法,当调用数组方法时会触发更新,也会对数组中的数据(对象类型)进行了监控
通过以上两点可以发现Vue中的缺陷:
- 对象默认只监控自带的属性,新增的属性响应式不生效 (层级过深,性能差)
- 数组通过索引进行修改 或者 修改数组的长度,响应式不生效
Vue额外提供的API:
vm.$set(vm.arr,0,100); // 修改数组内部使用的是splice方法
vm.$set(vm.address,'number','6-301'); // 新增属性通过内部会将属性定义成响应式数据
vm.$delete(vm.arr,0); // 删除索引,属性
为了解决以上问题,Vue3.0使用Proxy来解决
let obj = {
name: {name: 'jw'},
arr: ['吃', '喝', '玩']
}
let handler = {
get(target,key){
if(typeof target[key] === 'object' && target[key] !== null){
return new Proxy(target[key],handler);
}
return Reflect.get(target,key);
},
set(target,key,value){
let oldValue = target[key];
if(!oldValue){
console.log('新增属性')
}else if(oldValue !== value){
console.log('修改属性')
}
return Reflect.set(target,key,value);
}
}
let proxy = new Proxy(obj,handler);
代理 get、set方法,可以实现懒代理。并且兼容数组索引和长度变化
实例方法
- vm._uid (每个实例的唯一标识)
- vm.$data === vm._data (实例的数据源)
- vm.$options (用户传入的属性)
- vm.$el (当前组件的真实dom)
- vm.$nextTick (等待同步代码执行完毕)
- vm.$mount (手动挂载实例)
- vm.$watch (监控数据变化)
这些属性后续都会经常被应用,当然还有一些其他比较重要的属性
四.指令的使用
vue中的指令,vue中都是以v-开头 (一般用来操作dom
)
常见指令
v-once
渲染一次 (可用作优化,但是使用频率极少)v-html
将字符串转化成dom
插入到标签中 (会导致xss攻击问题,并且覆盖子元素)v-if/v-else/v-else-if
不满足时dom
不存在(可以使用template标签)v-show
不满足时dom
隐藏 (不能使用template标签)v-for
循环字符串、对象、数字、数组 (循环时必须加key,尽量不采用索引)v-bind
可以简写成: 属性(style、class…)绑定v-on
可以简写成@ 给元素绑定事件 (常用修饰符 .stop、.prevent、.self、.once、.passive)v-model
双向绑定 (支持.trim、.number修饰符)
v-show和v-if区别
- v-if 如果条件不成立不会渲染当前指令所在节点的 dom 元素
- v-show 只是切换当前 dom 的显示或者隐藏
const VueTemplateCompiler = require('vue-template-compiler');
let r1 = VueTemplateCompiler.compile(`
<div v-if="true"><span v-for="i in 3">hello</span></div>`
);
/** with(this) {
* return (true) ? _c('div', _l((3), function (i) { return _c('span', [_v("hello")]) }), 0) : _e()
* }
**/
v-show
会解析成指令,变为display:none
v-for和v-if连用问题
- v-for 会比 v-if 的优先级高一些,如果连用的话会把 v-if 给每个元素都添加一下,会造成性能问题 (使用计算属性优化)
const VueTemplateCompiler = require('vue-template-compiler'); let r1 = VueTemplateCompiler.compile(`<div v-if="false" v-for="i in 3">hello</div>`); /** with(this) { * return _l((3), function (i) { return (false) ? _c('div', [_v("hello")]) : _e() }) * } **/;
v-for为什么要加key
v-model原理
内部会根据标签的不同解析出,不同的语法
- 例如 文本框会被解析成 value + input事件
- 例如 复选框会被解析成 checked + change事件
- …
刨析Vue.js 内部运行机制(有兴趣可以了解)
- 响应式系统的基本原理
- 响应式系统的依赖收集追踪原理
- 实现Virtual DOM 下一个VNode节点
- template模板是怎样通过Compile编译的
- 数据状态更新是的差异diff及patch机制
- 批量异步更新策略及nextTick原理
-
五.自定义指令
我们可以自定义Vue中的指令来实现功能的封装 (全局指令、局部指令)
钩子函数
指令定义对象可以提供如下几个钩子函数:
bind:只调用一次,指令第一次绑定到元素时调用
- inserted:被绑定元素插入父节点时调用
- update:所在组件的 VNode 更新时调用,组件更新前状态
- componentUpdated:所在组件的 VNode 更新时调用,组件更新后的状态
- unbind:只调用一次,指令与元素解绑时调用。
// 1.el 指令所绑定的元素,可以用来直接操作 DOM // 2.bindings 绑定的属性 // 3.Vue编译生成的虚拟节点 (context)当前指令所在的上下文 bind(el,bindings,vnode,oldVnode){ // 无法拿到父元素 父元素为null console.log(el.parentNode,oldVnode) }, inserted(el){ // 父元素已经存在 console.log(el.parentNode) }, update(el){ // 组件更新前 console.log(el.innerHTML) }, componentUpdated(el){ // 组件更新后 console.log(el.innerHTML) }, unbind(el){ // 可用于解除事件绑定 console.log(el) }
clickOutSide
<div v-click-outside="hide">
<input type="text" @focus="show">
<div v-if="isShow">显示面板</div>
</div>
指令的编写
Vue.directive(clickOutside,{
bind(el,bindings,vnode){
el.handler = function (e) {
if(!el.contains(e.target)){
let method = bindings.expression;
vnode.context[method]();
}
}
document.addEventListener('click',el.handler)
},
unbind(el){
document.removeEventListener('click',el.handler)
}
})
六.vue-cli
项目创建
1.安装
npm install -g @vue/cli
npm install -g @vue/cli-service-global
vue create vue-ant-admin
2.初始化
? Check the features needed for your project:
(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
( ) Router
( ) Vuex
>(*) CSS Pre-processors
( ) Linter / Formatter
( ) Unit Testing
( ) E2E Testing
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default)
Sass/SCSS (with dart-sass)
Sass/SCSS (with node-sass)
Less
> Stylus
七.你需要了解的包管理工具与配置项
任何一个项目的构建离不开工具和统一的管理标准,在项目开发和维护过程中,我们需要了解安装包的相应工具和配置文件,以此来有效的进行项目的迭代和版本的更新,为项目提供基本的运行环境。
1. npm 与 package.json
npm 是 Node Package Manager 的简称,顾名思义,它是 node 的包管理工具,也是目前世界上最大的开源库生态系统。官方地址为:www.npmjs.com/,你可以在里面找到数以万计的开源包。
使用 npm 包下载量统计工具,比如 npm-start,我们可以查看相应包在一定时间范围内的下载量数据,下面是 vue-cli
和 @vue/cli
的下载量趋势:
介绍了使用 vue-cli 来构建自己的项目,并生成了相应的目录结构,而在最外层目录中,我们可以看到有 package.json
这一文件,该文件便是我们需要了解的包管理文件。
我们先来看一下该文件里面的内容:
{
"name": "my-project",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"vue": "^2.5.16",
"vue-router": "^3.0.1",
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.0.0-beta.15",
"@vue/cli-service": "^3.0.0-beta.15",
"less": "^3.0.4",
"less-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
可以看到该文件是由一系列键值对构成的 JSON 对象,每一个键值对都有其相应的作用,比如 scripts 脚本命令的配置,我们在终端启动项目运行的 npm run serve
命令其实便是执行了 scripts 配置下的 serve 项命令 vue-cli-service serve
,我们可以在 scripts 下自己修改或添加相应的项目命令。
而 dependencies 和 devDependencies 分别为项目生产环境和开发环境的依赖包配置,也就是说像 @vue/cli-service
这样只用于项目开发时的包我们可以放在 devDependencies 下,但像 vue-router
这样结合在项目上线代码中的包应该放在 dependencies 下。
详细的package.json文件配置项介绍可以参考:package.json
2. 常用命令
在简单的了解了 package.json 文件后,我们再来看下包管理工具的常用命令。一般在项目的构建和开发阶段,我们常用的 npm 命令有:
# 生成 package.json 文件(需要手动选择配置)
npm init
# 生成 package.json 文件(使用默认配置)
npm init -y
# 一键安装 package.json 下的依赖包
npm i
# 在项目中安装包名为 xxx 的依赖包(配置在 dependencies 下)
npm i xxx
# 在项目中安装包名为 xxx 的依赖包(配置在 dependencies 下)
npm i xxx --save
# 在项目中安装包名为 xxx 的依赖包(配置在 devDependencies 下)
npm i xxx --save-dev
# 全局安装包名为 xxx 的依赖包
npm i -g xxx
# 运行 package.json 中 scripts 下的命令
npm run xxx
比较陌生但实用的有:
# 打开 xxx 包的主页
npm home xxx
# 打开 xxx 包的代码仓库
npm repo xxx
# 将当前模块发布到 npmjs.com,需要先登录
npm publish
相比 npm,yarn 相信大家也不会陌生,它是由 facebook 推出并开源的包管理工具,具有速度快,安全性高,可靠性强等主要优势,它的常用命令如下:
# 生成 package.json 文件(需要手动选择配置)
yarn init
# 生成 package.json 文件(使用默认配置)
yarn init -y
# 一键安装 package.json 下的依赖包
yarn
# 在项目中安装包名为 xxx 的依赖包(配置在 dependencies 下),同时 yarn.lock 也会被更新
yarn add xxx
# 在项目中安装包名为 xxx 的依赖包(配置在配置在 devDependencies 下),同时 yarn.lock 也会被更新
yarn add xxx --dev
# 全局安装包名为 xxx 的依
yarn global add xxx
# 运行 package.json 中 scripts 下的命令
yarn xxx
比较陌生但实用的有:
# 列出 xxx 包的版本信息
yarn outdated xxx
# 验证当前项目 package.json 里的依赖版本和 yarn 的 lock 文件是否匹配
yarn check
# 将当前模块发布到 npmjs.com,需要先登录
yarn publish
以上便是 npm 与 yarn 包管理工具的常用及实用命令,需要注意的是,优先使用 yarn 命令进行包的管理和安装。
3. 第三方插件配置
在上方的 package.json 文件中我们可以看到有 browserslist 这一配置项,那么该配置项便是这里所说的第三方插件配置,该配置的主要作用是用于在不同的前端工具之间共享目标浏览器和 Node.js 的版本:
"browserslist": [
"> 1%", // 表示包含所有使用率 > 1% 的浏览器
"last 2 versions", // 表示包含浏览器最新的两个版本
"not ie <= 8" // 表示不包含 ie8 及以下版本
]
比如像 autoprefixer 这样的插件需要把你写的 css 样式适配不同的浏览器,那么这里要针对哪些浏览器呢,就是上面配置中所包含的。
而如果写在 autoprefixer 的配置中,那么会存在一个问题,万一其他第三方插件也需要浏览器的包含范围用于实现其特定的功能,那么就又得在其配置中设置一遍,这样就无法得以共用。所以在 package.json 中配置 browserslist 的属性使得所有工具都会自动找到目标浏览器。
当然,你也可以单独写在 .browserslistrc 的文件中:
# Browsers that we support
> 1%
last 2 versions
not ie <= 8
至于它是如何去衡量浏览器的使用率和版本的,数据都是来源于 Can I Use。你也可以访问 browserl.ist/ 去搜索配置项所包含的浏览器列表,比如搜索 last 2 versions
会得到你想要的结果,或者在项目终端运行如下命令查看:
npx browserslist
除了上述插件的配置,项目中常用的插件还有:babel、postcss 等,有兴趣的可以访问其官网进行了解。
八.Vue组件通信
常见组件通信方式
对于Vue组件间的数据通信,无外呼是父组件向子组件、子组件向父组件、兄弟组件以及嵌套组件之间的数据通信。而且组件之间的通信方式也有很多种。
props
和$emit
父组件向子组件传递数据是通过prop
传递的,子组件传递数据给父组件是通过$emit
触发事件来做到的$attrs
和$listeners
A->B->C。Vue 2.4 开始提供了$attrs
和$listeners
来解决这个问题$parent
,$children
$refs
获取实例- 父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。
envetBus
平级组件数据传递 这种情况下可以使用中央事件总线的方式vuex
状态管理
《$attrs 和 $listeners》
《Vue 父子组件通信的十种方式》一文就详细的介绍了Vue组件,指的是父子组件之间的数据通信就有差不多十种方式。但很多时候我们组件之间的数据通信不仅仅是停留在父子组件之间的数据通信。比如说还有兄弟组件和嵌套组件之间的数据通信。
九.学会编写可复用性模块
- 开发前先思考,然后沟通,如果基本组件,可以多参考第三组件如何编写的,多参考,借鉴,如果是业务组件,需要多方参与进来开发的,如后端、产品、ui等。
- 划分组件结构,如何做到颗粒化。
- 数据如何通信,需要哪些设置参数给外部使用的
- 事件
- 测试
在生活中,重复的机械劳动会消耗我们的时间和精力,提高生产成本,降低工作效率。同样,在代码世界中,编写重复的代码会导致代码的冗余,页面性能的下降以及后期维护成本的增加。由此可见将重复的事情复用起来是提高生产效率、降低维护成本的不二之选。
在 Vue 项目中,每一个页面都可以看作是由大大小小的模块构成的,即便是一行代码、一个函数、一个组件都可以看作是一个个自由的模块。那么提高代码的复用性的关键便在于编写可复用的模块,也就是编写可复用的代码、函数和组件等。
一个简单的例子
let person = [];
for (let i = 0; i < data.obj.items.length; i++) {
person.push({
name: data.obj.items[i].name,
age: data.obj.items[i].age
});
}
不知道上方代码给你的第一印象是什么?总之给我的印象是糟糕的,因为出现了重复性的代码片段 data.obj.items
,可能这样的代码在我们团队开发中随处可见,这也说明了重复编码现象其实无处不在。
面对自己编写的代码,我们应该保持一颗去重的心,发现重复的地方就相当于找到了可以复用的模块。在不复用的情况下,上述代码一旦需要修改变量 items
为 lists
,那么我们就得修改 3 处地方,不知不觉就增加了维护成本。而到时候往往修改你代码的人并不是你自己,所以对自己好点,对他人也会好点。复用后的代码如下:
let person = [];
let values = data.obj.items;
for (let i = 0; i < values.length; i++) {
person.push({
name: values[i].name,
age: values[i].age
});
}
我们通过将 data.obj.items 的值赋值给变量 values 来实现了复用,此时修改 items
为 lists
的话我们只需修改一处地方即可,不管是维护成本还是代码可读性上,复用的优势都显而易见。
封装成一个函数
除了使用变量的赋值缓存使用来解决数据的重复读取外,我们在开发过程中重复性更多的也许是功能点的重复,比如:
<tempalte>
<div>
<input type="text" v-model="str1">
<input type="text" v-model="str2">
<div>{{ str1.slice(1).toUpperCase() }}</div>
<div>{{ str2.slice(1).toUpperCase() }}</div>
</div>
</template>
上述代码的重复功能点在于截取输入框中第二个字符开始到最后的值并把它们转化成大写字母,像这样很简单的操作虽然重复使用也不会出现太大的问题,但是如果是代码量较多的操作呢?重复书写相同功能的代码是一种不经过大脑思考的行为,我们需要对其进行优化,这里我们可以把功能点封装成一个函数:
export default {
methods: {
sliceUpperCase(val) {
return val.slice(1).toUpperCase()
}
}
}
如此我们只要在用到该方法的地方调用即可,将值传入其中并返回新值。当然像在双花括号插值和 v-bind 表达式中重复的功能点我们可以封装成过滤器比较合适:
// 单文件组件注册过滤器
filters: {
sliceUpperCase(val) {
return val.slice(1).toUpperCase()
}
}
// 全局注册过滤器
Vue.filter('sliceUpperCase', function (val) {
return val.slice(1).toUpperCase()
})
然后在 html 中使用“管道”符进行过滤:
<div>{{ str1 | sliceUpperCase }}</div>
<div>{{ str2 | sliceUpperCase }}</div>
这样我们就把重复的功能性代码封装成了函数,而不管是过滤器还是正常的方法封装,其本质都是函数的封装。
封装成一个组件
相比较于函数的封装,规模更大一点的便是组件的封装,组件包含了模板、脚本以及样式的代码,在实际开发中组件的使用频率也是非常大的,我们项目中的每一个页面其实都可以看作是一个父组件,其可以包含很多子组件,子组件通过接收父组件的值来渲染页面,父组件通过响应子组件的回调来触发事件。
封装一个组件主要包含两种方式,一种是最常见的整体封装,用户通过改变数据源来呈现不同的页面状态,代码结构不可定制化。例如:
<div>
<my-component data="我是父组件传入子组件的数据"></my-component>
</div>
另一种便是自定义封装,也就是插槽(slot),我们可以开放一部分槽位给父组件,使其能够进行一定程度的定制化,例如:
<div>
<my-component data="我是父组件传入子组件的数据">
<template slot="customize">
<span>这是定制化的数据</span>
</template>
</my-component>
</div>
在 myComponent 组件中我们便可以接收对应的 slot:
<div class="container">
<span>{{ data }}</span>
<slot name="customize"></slot>
<div>
这里我们通过定义 slot 标签的 name 值为 customize 来接收父组件在使用该组件时在 template 标签上定义的 slot=”customize” 中的代码,不同父组件可以定制不同的 slot 代码来实现差异化的插槽。最终渲染出来的代码如下:
<div>
<div class="container">
<span>我是父组件传入子组件的数据</span>
<span>这是定制化的数据</span>
</div>
</div>
这样我们就完成了一个小型组件的封装,将共用代码封装到组件中去,页面需要引入的时候直接使用 import 并进行相应注册即可,当然你也可以进行全局的引入:
import myComponent from '../myComponent.vue'
// 全局
Vue.component('my-component', myComponent)
封装成一个插件
在某些情况下,我们封装的内容可能不需要使用者对其内部代码结构进行了解,其只需要熟悉我们提供出来的相应方法和 api 即可,这需要我们更系统性的将公用部分逻辑封装成插件,来为项目添加全局功能,比如常见的 loading 功能、弹框功能等。
Vue 提供给了我们一个 install 方法来编写插件,使用该方法中的第一个 Vue 构造器参数可以为项目添加全局方法、资源、选项等。比如我们可以给组件添加一个简单的全局调用方法来实现插件的编写:
/* toast.js */
import ToastComponent from './toast.vue' // 引入组件
let $vm
export default {
install(Vue, options) {
// 判断实例是否存在
if (!$vm) {
const ToastPlugin = Vue.extend(ToastComponent); // 创建一个“扩展实例构造器”
// 创建 $vm 实例
$vm = new ToastPlugin({
el: document.createElement('div') // 声明挂载元素
});
document.body.appendChild($vm.$el); // 把 toast 组件的 DOM 添加到 body 里
}
// 给 toast 设置自定义文案和时间
let toast = (text, duration) => {
$vm.text = text;
$vm.duration = duration;
// 在指定 duration 之后让 toast 消失
setTimeout(() => {
$vm.isShow = false;
}, $vm.duration);
}
// 判断 Vue.$toast 是否存在
if (!Vue.$toast) {
Vue.$toast = toast;
}
Vue.prototype.$toast = Vue.$toast; // 全局添加 $toast 事件
}
}
成功编写完插件的 JS 脚本后,我们在入口文件中需要通过 Vue.use() 来注册一下该插件:
import Toast from '@/widgets/toast/toast.js'
Vue.use(Toast); // 注册 Toast
最后我们在需要调用它的地方直接传入配置项使用即可,比如:
this.$toast('Hello World', 2000);
当然你也可以不使用 install 方法来编写插件,直接采用导出一个封装好的实例方法并将其挂载到 Vue 的原型链上来实现相同的功能。
更详细的编写插件和实例的方法:Vue 插件编写与实战
十.表单组件的封装
- 掌握插槽的应用
- $parent、$children、provide和inject的使用
- 组件的双向数据绑定
1.表单的使用
<template>
<div>
<fd-form
:model="detail"
:schema="schema"
:options="options"
:btn-del="MEETING_ID"
:title="MEETING_ID ? '会议记录编辑': '会议记录新增'"
:btn-edit="MEETING_ID"
is-btn
is-back="back"
@submit="submitHandler"
@delete="deleteHandler"
@back="back"
/>
</div>
</template>
<script>
export default {
components: {
fdForm: () => import('@/components/form/form'),
loading: () => import('@/components/loading')
},
data() {
return {
ruleForm: {
username: "",
password: ""
},
rules: {
username: [
{ required: true, message: "请输入用户名" },
{ min: 3, max: 5, message: "长度在 3 到 5 个字符" }
],
password: [{ required: true, message: "请输入密码" }]
}
};
},
methods: {
submitHandler(params) {}
};
</script>
这里我们参考
cube-ui
表单组件的使用,自己动手实现下这三个组件。通过这三个组件的应用来掌握内部通信的机制。
├── form # 表单组件
│ ├── field # 表单组件所方法
│ ├── components.js # 表单组件所需要组件
│ ├── form.vue # 表单组件
│ ├── form-group.vue # 表单组件
│ ├── form-item.vue # 表单组件
│ ├── layouts.js # 表单组件所需要常量
│ ├── minxin.js # 表单组件所需要全局变量
import FdInput from '../input/input.vue'
import FdSelect from '../select/select.vue'
import FdTimePicker from '../timePicker/timePicker.vue'
import FdChecker from '../checker/checker.vue'
import FdTree from '../formUserSelect/treeSelect.vue'
import FdTextarea from '../textarea/textarea.vue'
import FdSwitch from '../switch/switch.vue'
import FdUpload from '../upload/upload.vue'
import FdPopupRadio from '../popupRadio/index.vue'
import FdAddInput from '../addInput/addInput.vue'
const allComponents = [
FdInput,
FdSelect,
FdTimePicker,
FdChecker,
FdTree,
FdTextarea,
FdSwitch,
FdUpload,
FdPopupRadio,
FdUpload,
FdAddInput
]
const components = {}
allComponents.forEach((Component) => {
components[Component.name] = Component
})
export default components
使用时:
componentName() {
const fieldValue = this.fieldValue
const component = fieldValue.component
if (component) {
return component
}
const type = fieldValue.type
const fdType = `fd-${type}`
if (components[fdType]) {
return fdType
}
return type
}
<template>
<div class="fd-form-group">
<p v-if="legend" class="fd-form-group-legend">{{ legend }}</p>
<div class="fd-form-group-content">
<slot>
<fd-form-item
v-for="(field, index) in fields"
:key="index"
:field="field"
/>
</slot>
</div>
</div>
</template>
使用slot 插槽,实现自定义表单
<fd-form :model="model">
<fd-form-group>
<fd-form-item :field="fields[0]"/>
<fd-form-item :field="fields[1]">
<span class="field-first">{{ model.deptName }}</span>
</fd-form-item>
<fd-form-item :field="fields[2]"/>
<fd-form-item :field="fields[3]"/>
<!-- 0未提交 1预定 2未预定 -->
<fd-form-item v-if="orderStatue!=='0'" :field="fields[4]">
<span v-if="orderStatue==='0'">未提交</span>
<span v-if="orderStatue==='1'">已预订</span>
<span v-if="orderStatue==='2'">未预订</span>
</fd-form-item>
</fd-form-group>
<div v-if="orderStatue==='0'" class="pro-button-box">
<span @click="orderHandler('1')">预订</span>
<span @click="orderHandler('2')">不预订</span>
</div>
</fd-form>
<script>
components: {
fdForm: () => import('@/components/form/form'),
fdFormGroup: () => import('@/components/form/form-group'),
fdFormItem: () => import('@/components/form/form-item')
},
</script>
有兴趣了解更多,可以在库找
https://github.com/zhangbinzhbb/fz-app/tree/master/src/components
十一.作用域插槽
普通插槽
<template>
<div>
<div>我是子组件</div>
<p>现在测试一下slot</p>
<slot></slot>
</div>
</template>
<myslot>
<p>测试一下吧内容写在这里了能否显示</p>
</myslot>
具名插槽
<template>
<div class="container">
<header>
<!-- 我们希望把页头放这里 -->
</header>
<main>
<!-- 我们希望把主要内容放这里 -->
</main>
<footer>
<!-- 我们希望把页脚放这里 -->
</footer>
</div>
</template>
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
一个不带 name
的 <slot>
出口会带有隐含的名字“default”。 父组件在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称:
<template>
<myslot>
<div>大家好我是父组件</div>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's footer info</p>
</template>
</myslot>
</template>
作用域插槽
v-bind:userData=”user”
v-slot=”slotProps
<template>
<myslot v-slot="slotProps">
{{ slotProps.user.firstName }}
<template v-slot:other="otherSlotProps">
slotProps is NOT available here
</template>
</myslot>
</template>
<template>
<div>
<span>
<slot v-bind:userData="user" name="header">
{{ user.msg }}
</slot>
<slot v-bind:hobbyData="hobby" name="footer">
{{ hobby.fruit }}
</slot>
</span>
</div>
</template>
<script>
export default {
data () {
return {
user:{
firstName: 'gerace',
lastName: 'haLi',
},
hobby:{
fruit: "apple",
color: "blue"
}
}
}
}
</script>
<style>
</style>
十二.项目实战
项目目录
点击文章底部的《vue-ant-admin 项目模版》链接
路由系统配置
通过require.context
实现路由模块拆分
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter);
const routes = [];
const files = require.context('./', true, /\.router.js$/);
files.keys().forEach(key => {
routes.push(...files(key).default)
});
const router = new VueRouter({
mode: 'history',
routes
});
export default router;
通过
require.context
动态导入路由模块,实现路由的模块化,这样我们可以对路由进行分类了 (这里不建议根据页面自动生成路由,这样项目整个太不灵活了)
index.router.js
export default [{
path: '/',
component: () => import(/*webpackChunkName:'home'*/'@/views/Home.vue')
}, {
path: '*',
component: () => import(/*webpackChunkName:'404'*/'@/views/404.vue')
}]
user.router.js
export default [{
path: '/login',
name: 'login',
component: () => import( /*webpackChunkName:'login'*/ '@/views/user/Login.vue')
},
{
path: '/reg',
name: 'reg',
component: () => import( /*webpackChunkName:'reg'*/ '@/views/user/Reg.vue')
}
]
合理划分容器组件与展示组件
头部导航栏(Navbar) | ||||
---|---|---|---|---|
侧边菜单 | 内容区域(AppMain) | |||
<template>
<div class="app-wrapper">
<Navbar @mouseenter.native="mouseenterEvent" />
<div class="main-container">
<Sidebar ref="sidebar"
class="sidebar-container"
@dialog-show="maskShow = true" />
<AppMain :class="maskShow?'sider-class-active':''"
@mouseenter.native="mouseenterEvent" />
</div>
<div class="drawer-section"
:class="visible?'drawer-section-open':''"
@click="showDrawer">
<a-drawer title="设置主题"
width="276px"
placement="right"
:closable="false"
:mask="false"
:after-visible-change="afterVisibleChange"
:visible="visible"
@close="onClose">
<Setting />
</a-drawer>
<a-icon :type="visible?'close':'setting'" />
</div>
<div v-if="maskShow"
class="yu-drawer-mask"
@mouseenter="mouseenterEvent"></div>
</div>
</template>
封装导航组件
├── Header
│ ├── HeaderSearch.vue
│ ├── index.less
│ ├── Navbar.vue
公共设施配置
最后我们项目开发中肯定需要对一些公共的方法进行封装使用,这里我把它称之为公共设施,那么我们可以在 src 目录下建一个 utils 文件夹来存放其配置文件:
└── src
└── utils
├── index.js # 公共配置入口
├── validate.js # 表单验证配置
└── other.js # 其他配置
...
十三.Vuex
配置
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式
1.简单store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
}
})
该配置文件便是 Vuex 的配置文件,主要有 4 个核心点:state、mutations、actions 及 getter,详细的介绍大家可以参考官方文档:核心概念,这里我用一句话介绍它们之间的关系就是:我们可以通过 actions 异步提交 mutations 去 修改 state 的值并通过 getter 获取。
需要注意的是不是每一个项目都适合使用 Vuex,如果你的项目是中大型项目,那么使用 Vuex 来管理错综复杂的状态数据是很有帮助的,而为了后期的拓展性和可维护性,这里不建议使用 CLI 生成的一份配置文件来管理所有的状态操作,我们可以把它拆分为以下目录:
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── getter.js # 根级别的 getter
└── modules
├── moduleA.js # A模块
└── moduleB.js # B模块
与单个 store.js 文件不同的是,我们按模块进行了划分,每个模块中都可以包含自己 4 个核心功能。比如模块 A 中:
/* moduleA.js */
import * as menu from "@/api/menu";
const state = {
menuLeftList: []
}
const mutations = {
UPDATE_MENU_LIST: (state, data) => {
// 这里的 `state` 对象是模块的局部状态
state.menuLeftList = data;
},
}
const actions = {
setMenu({commit}){
menu.getMenu().then((res)=>{
commit("UPDATE_MENU_LIST",res)
})
}
}
export default {
namespaced:true,
state,
mutations,
actions
}
2.模块的基本配置 index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const rootModule = {
state: {},
mutations: {},
actions: {},
modules: {},
};
const files = require.context("./modules/", false, /\.js$/, );
files.keys().forEach((key) => {
let store = files(key).default;
const moduleName = key.replace(/^\.\//, "").replace(/\.js$/, "");
const modules = rootModule.modules || {};
modules[moduleName] = store;
modules[moduleName].namespaced = true;
rootModule.modules = modules;
});
const store = new Vuex.Store(rootModule);
export default store;
通过
require.context()
动态加载模块,实现store的状态分割
这样项目中状态的模块划分就更加清晰,对应模块的状态我们只需要修改相应模块文件即可。详细的案例代码可参考文末 github 地址。
在组件中获取数据
<script>
import { mapState,mapMutations } from "vuex";
export default {
computed: {
...mapState("user", ["menuLeftList"]),
...mapGetters(["lang","strageList"])
},
methods:{
...mapMutations("setting",["UPDATE_MENU_LIST"])
delMenuItem(code) {
this.$store.dispatch("user/updateMenuList", code);
},
}
}
</script>
十四.页面使用如何请求
在项目的开发过程中,我们也少不了与后台服务器进行数据的获取和交互,这一般都是通过接口完成的,那么我们如何进行合理的接口配置呢?我们可以在 src 目录下新建 api 文件夹用于存放接口文件:
└── src
└── api
├── moduleA.js # A模块接口
└── moduleB.js # B模块接口
└── util
├── request.js # 接口封装
为了让接口便于管理,我们同样使用不同的文件来配置不同模块的接口,同时由于接口的调用 ajax 请求代码重复部分较多,我们可以对其进行简单的封装,比如在 request.js 中(axios为例):
封装request请求
每次请求时通过axios.create()
方法创建axios实例并增添拦截器功能。
/*
* @Author: zhangbb
* @Date: 2020-09-14 14:11:59
* @Last Modified by: zhangbb
* @Last Modified time: 2020-09-22 14:11:59
*/
import axios from "axios";
import {
Message
} from "ant-design-vue";
import {
BASE_URL,
} from "./config";
import store from "@/store";
import {
getToken
} from "@/utils/auth";
// create an axios instance
const service = axios.create({
baseURL: BASE_URL, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
});
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['Authorization'] is a custom headers key
// please modify it according to the actual situation
// jwt
config.headers["Authorization"] = getToken();
}
return config;
},
error => {
// do something with request error
console.log(error); // for debug
return Promise.reject(error);
}
);
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data;
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 20000) {
console.log('res:', res);
Message.error(
res.message || "Error",
5,
);
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
store.dispatch("user/resetToken").then(() => {
location.reload();
});
// to re-login
// MessageBox.confirm(
// "You have been logged out, you can cancel to stay on this page, or log in again",
// "Confirm logout", {
// confirmButtonText: "Re-Login",
// cancelButtonText: "Cancel",
// type: "warning"
// }
// ).then(() => {
// store.dispatch("user/resetToken").then(() => {
// location.reload();
// });
// });
}
return Promise.reject(new Error(res.message || "Error"));
} else {
return res;
}
},
error => {
console.log("err" + error); // for debug
Message.error(
error.message,
5,
);
return Promise.reject(error);
}
);
export default service;
moduleA.js
import request from '@/utils/request';
/**
* 获取菜单左侧列表权限
*/
export function getList() {
return request({
url: '/vue-antd-admin/menu/list',
method: 'get'
});
}
/**
* 更新菜单左侧列表权限
*/
export function updateMenu(data) {
return request({
url: `/vue-antd-admin/menu/list/update`,
method: 'post',
data
});
}
在项目页面中进行调用
import * as moduleA from "@/api/moduleA";
moduleA.getList().then((res)=>{})
这时候我们再次调用接口的时候会发现其调用地址为本地地址:[http://127.0.0.1:8080/api/xxx/xxx.rb](http://127.0.0.1:8080/repos/octokit/octokit.rb)
,那么为了让其指向 [https://api.github.com](https://api.github.com)
,我们需要在 vue.config.js 中进行 devServer 的配置:
/* vue.config.js */
module.exports = {
...
devServer: {
// string | Object 代理设置
proxy: {
// 接口是 '/api' 开头的才用代理
'/api': {
target: 'https://api.github.com', // 目标地址
changeOrigin: true, // 是否改变源地址
// pathRewrite: {'^/api': ''}
}
},
}
...
}
在 devServer 中 我们配置 proxy 进行接口的代理,将我们本地地址转换为真实的服务器地址。
十五.路由钩子鉴权 permission.js
十六.扩充你的开发工具
在项目开发中,工具的使用起到了至关重要的作用,正所谓工欲善其事,必先利其器,掌握一些实用的开发工具能够使我们的开发效率事半功倍。
那么我们应该掌握哪些开发工具的使用方法呢?开发工具包括了 npm
、yarn
、webpack
以及一些集成在项目中的工具包,这些工具一定程度上都大大简化了我们的开发流程,起到了项目助推剂的作用。因此在开发工具的学习上我们应该抱着宜多不宜少的心态,积极主动的扩充自己的工具库。
巧用 Chrome 插件
首先,既然说到工具,那我们不得不介绍下占据浏览器市场份额霸主地位的 Chrome
了。相信每一个从事前端开发的同学都对其寄存着一种亲切感,因为只要是参与 web 项目的开发就基本上离不开它的关照,比如它提供的调试控制台以及数以万计的插件等。
而作为一名前端开发人员,我想你的 Chrome 浏览器地址栏右侧肯定排列着几款你钟爱的插件,使用的插件数量越多说明了你掌握的 Chrome 技能越多,同时一定程度上也凸显了你的开发能力。
那么接下来我们不妨来认识一下几款实用的 Chrome 插件:
Vue.js devtools
首先介绍的肯定是 Vue.js devtools,它是 Vue 官方发布的一款调试 Vue 项目的插件,支持数据模拟与调试。相信从事过 Vue 项目开发的同学都已经把它收入在自己的工具库中了,它的界面如下:
成功安装它之后,在 Vue 项目的页面中我们可以打开 Chrome 控制台选择 Vue 的 tab 进行页面调试。
Vue Performance Devtool
Vue Performance Devtool 这款插件,它可以分析我们页面中各个组件的性能情况,从而在其基础上我们可以有针对性的对组件的代码进行优化,如下图所示:
同样安装完毕后,我们可以打开 Chrome 控制台选择 Vue Performance
的 tab 进行组件的性能观察。
Postman
Postman 相信大家都比较熟悉,它是一款非常好用的接口调试工具。在 Vue 项目开发中,我们或多或少需要对后台提供的接口进行测试,比如传递数据并查看返回结果等,这时候使用 Postman 便可以完成这些任务。
Postman 会当作 Chrome 应用程序安装到你的电脑上,打开后我们可以选择请求方式(GET/POST),输入请求 URL 以及设置传递参数来进行接口的调用。
Web Developer
Web Developer 是一款强大的用于操作网页中各项资源与浏览器的插件,比如一键禁用 JS、编辑 CSS、清除 Cookie 等。
虽然说一些功能我们也可以在 Chrome 控制台实现,但其提供的快捷键能够十分方便的让我们在页面中操作某些资源。
Google PageSpeed Insights API Extension
PageSpeed Insights (PSI) 是 Google
在全球范围内应用最广的开发者工具之一,其中文网页版 developers.google.cn/speed/pages… 也已经发布。作为一款专注于改进网页性能的开发者工具,它主要具有以下两个优势:真实的网页运行速度 及 优化建议。
为了便于使用,我们可以直接下载 Chrome 插件 Google PageSpeed Insights API Extension 来对当前访问网址进行测试和分析。
FeHelper
FeHelper 是百度 FE 团队开发的一款前端工具集插件,包含代码压缩/性能检测/字符串编解码等功能,能够帮助我们完成一些琐碎的开发任务。
FeHelper 为我们提供了十多种快捷功能,在需要的时候我们直接点击插件图标选择对应功能即可,操作起来十分便捷。
Can I Use
Can I Use 是 caniuse.com/ 网页版的插件。我们可以使用其来查看某一特性的浏览器支持程度,确保主流浏览器的支持。
使用 Chrome 插件形式的 Can I Use 我们可以快捷的查看项目中用到的某一特性的浏览器支持范围,同时还可以查看支持程度和兼容方式。
其他实用插件
- JSONView :一款可以将后台返回的 JSON 字符串数据自动格式化成规范 JSON 格式的插件
- WhatFont:一款可以显示浏览器中选择文字的字体类型/字号/颜色的插件
- The QR Code Extension:一款允许当前页面生成二维码,并使用网络摄像头扫描二维码的插件
- Test IE:一款可以模拟
IE
及其他主流浏览器的插件,但大部分模拟场景需要付费才能使用 - Wappalyzer:一款查看当前网站使用的前后端技术的插件,帮助你学习和认识优秀网站的技术选型
- Mobile/Responsive Web Design Tester:一款用于测试页面在不同机型下呈现的插件
- Resolution Test:一款用于测试页面在不同分辨率下呈现的插件
以上我们介绍了一些非常实用的 chrome 拓展插件来助力我们的前端开发,为项目开发提供了工具解决方案,同时也有助于帮助大家开启以工具为向导的开发模式。
分析你的包文件
每当我们使用 webpack 打包项目代码的时候,你可能需要关注一下打包生成的每个 js 文件的大小以及其包含的内容,这对于优化项目打包速度和提升页面加载性能都有十分大的帮助。
这里我们推荐使用 webpack-bundle-analyzer 这一款 webpack 插件来进行包文件的分析,下面我们就来介绍下其配置和使用方法。
首先作为一款需要内置在代码中的开发分析工具,我们需要安装并在 webpack 的 plugins 中添加该插件:
# 安装命令
yarn add webpack-bundle-analyzer --dev
/* vue.config.js */
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const isPro = process.env.NODE_ENV === 'production'
module.exports = {
...
configureWebpack: config => {
if (isPro) {
return {
plugins: [
// 使用包分析工具
new BundleAnalyzerPlugin()
]
}
}
},
...
}
这样我们在生产环境下打包便可以在浏览器 8888 端口(默认)下打开页面进行包文件的分析,如下图所示:
图中区域内包含了我们打包出的所有 js 文件,我们可以以不同的颜色进行区分,同时我们也可以点击某一区块进行放大观察,以此来分析是否存在较大或重复的模块。而在页面左侧存在一个筛选面板,在该面板中我们能勾选需要查看的文件来进行显示,同时也可以切换查看原始、普通及 GZIP
压缩模式下的文件大小。
使用好 webpack-bundle-analyzer 工具我们可以快速的找到需要合并的模块,解决文件冗余,为资源优化提供可行性方案。
调试移动端页面
除了 Chrome 插件及打包分析工具的介绍外,我们再来了解下移动端页面的调试工具。相比 PC 端调试,移动端调试可能稍微复杂一点,但是只要熟练的使用好 “工具” 这一东西,我们同样可以在移动端的世界中游刃有余。
作为一名 Mac
及 iOS
用户,这里我主要介绍在 iPhone
手机中调试页面的方法,当然最后也会简单介绍一下 Android
手机页面的调试方法。
首先我们得具备这些工具:iPhone 手机一部、数据线一条、Mac 电脑一台。在满足以上要求后我们需要把手机通过数据线连接上 Mac 电脑,连接完毕后便可以进行如下步骤的设置:
1. 打开苹果手机的 Web 检查器
(设置 > Safari浏览器 > 高级 > Web检查器),一般情况下默认是开启的
2. 打开 Mac 上的 Safari
的 “开发”菜单
,一般情况下默认是开启的
3. 在手机 Safari 浏览器中打开你需要调试的页面
4. 在 Mac Safari 浏览器中选择你需要调试的页面(开发 > 你的 iPhone > 你的页面地址)
5. 点击地址后弹出如图所示的控制台,你便可以在该控制台中进行调试了
最后你可以针对你的移动端页面进行断点调试、操作缓存、查看网络及资源等,帮助你快速的定位和解决问题。
而在 Android 手机中,我们同样可以对移动端页面进行调试,主要不同点在于 IOS 使用的工具是 iPhone 和 Mac,Android 使用的工具主要是 Android 手机和 Windows 系统罢了(Mac 也可以使用模拟器),当然还需要借助 Chrome 的帮助。
这里主要介绍一下 Chrome 中的 inspect
,我们可以在 Chrome 地址栏输入:chrome://inspect/
来捕获手机访问的页面地址,前提是你的 Android 手机通过数据线连接上了电脑并开启了相应权限,最后获取到的地址会在 Remote Target
中显示:
点击相应的地址会弹出一个控制台,你可以在该控制台中进行页面的调试。
十七项目上手前,需要怎么快速进入开发
1、拉取项目模板
2、开发工具,vscode、svn
3、了解开发前的文档,如前端规范、前端项目模板文档等
4、需掌握必要知识点,如vue、es、less
5、组件库
文档参考
WinCloud 组件设计规范v1.0:https://docs.qq.com/doc/DUkh0dEVTYWdUTEp3
https://www.yuque.com/docs/share/e8478254-9948-4341-9b20-6032a74803f1?# 《Ant-Design-Vue更换主题色》
https://www.yuque.com/docs/share/5fc895a2-4381-4b2a-bbbd-d11b3c6831b2?# 《前端自动化测试》
https://www.yuque.com/docs/share/5e035597-9d6c-4a4c-ae5f-ffea41d4917b?# 《vue-ant-admin 项目模版》
https://www.yuque.com/docs/share/3abf5d6c-f584-4b38-b17a-e8caabd0b799?# 《前端第一次技术分享》
https://www.yuque.com/docs/share/84eb81d6-0aff-4fdd-9609-f8fbc1433943?# 《前端开发规范》