目录
醉后不知天在水,满船清梦压星河。
码字不易,喜欢就点个关注❤,持续更新技术内容。
接上文:
第一篇:原生JS到Vue前端工程化开发_Maxlec的博客-CSDN博客
1 前端路由VueRouter
Vue路由vue-router是官方的路由插件(土一点说就是用来设置和管理组件超链接的),能够轻松的管理SPA项目(单页Web应用)中组件的切换,实现组件页面的跳转,单独展现出不同的组件页面。Vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。vue-router目前有3.x版本和4.x版本,3.x版本结合vue2进行使用,4.x版本结合vue3进行使用。
组成:
vuerouter对象:路由表,根据路由请求在路由视图中动态渲染选中的组件
<router-link>:请求链接组件,浏览器会解析成超链接<a>
<router-view>:动态路由组件,用来渲染展示与路由路径对应的组件
1.1 VueRouter安装
安装:npm install vue-router@3,安装vue-router3最新的版本
1.2 创建路由组件
接下我们通过路由来开发和管理单页面应用QQ音乐部分组件,可以将想看组件页面单独展现出来。
在项目中定义Musichall.vue、Mymusic.vue两个组件,将来要使用vue-router来控制它们的展示和切换:
Musichall.vue
<template>
<div>
<h1>音乐馆</h1>
</div>
</template>
Mymusic.vue
<template>
<div>
<h1>我的音乐</h1>
</div>
</template>
1.3 声明路由链接和占位符
创建好两个组件后,不用直接导入和注册到App.vue中全部显示,而是在App.vue中使用<router-link>标签来声明路由链接,并使用<router-view>标签来声明路由占位符,这样就可以实现在单页面下动态地进行组件的切换和单独展示:
<!-- 声明路由链接,并未设置链接与组件的对应关系 -->
<router-link to="/musichall">音乐馆</router-link>
<router-link to="/mymusic">我的音乐</router-link>
<!-- 声明路由占位标签 -->
<router-view></router-view>
1.4 创建路由模块
然后在项目中router文件夹下创建index.js路由模块,设置路由链接与组件的对应关系:
import VueRouter from 'vue-router'
import Vue from 'vue'
import Musichall from '../components/Musichall'
Vue.use(VueRouter)
// 在index.js文件中设置路由链接与组件的对应关系,注意对应关系属性名称是routes
const router = new VueRouter({
//指定hash属性path与组件的对应关系
routes: [
{path: '/musichall', component: Musichall},
//通过以下直接引入也行
{path: '/mymusic', component: () => import Mymusic from '../components/Mymusic'}
]
})
export default router//导出对应关系,然后在入口文件main.js中导入加同vue对象一起加载才生效。
最后再导入和设置路由使其在vue对象中生效:
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router: router,
}).$mount('#app')
这样如Musichall组件与在App.vue中声明的路由链接"/musichall"一一对应起来,这样以后在App.vue组件页面中切换到该链接,就会显示Musichal组件的页面信息,也就是router-view占位符的地方。
1.5 路由重定向
路由重定向指的是:如用户在访问根组件的时候,默认用户重定向到根组件下的相应组件,从而展示特定的组件页面。通过路由规则的redirect属性,指定一个新的路由地址,可以很方便地设置路由地重定向:
const router = new VueRouter({
//指定hash属性path与组件的对应关系
routes: [
{path: '/', redirect: '/musichall'},
{path: '/musichall', component: Musichall},
{path: '/mymusic', component: Mymusic}
]
})
1.6 嵌套路由
在Musichall组件中,可以声明topist和playlist地子路由链接以及子路由占位符:
<template>
<div>
<h1>音乐馆</h1>
<!-- 子路由链接 -->
<router-link to="/musichall/playlist">首页</router-link>
<router-link to="/musichall/singers">歌手</router-link>
<hr>
<router-view></router-view>
</div>
</template>
在index.js路由对应关系文件中,导入需要的组件,并使用children属性声明子路由规则:
// 在index.js文件中设置路由链接与组件的对应关系
const router = new VueRouter({
//指定hash属性path与组件的对应关系
routes: [
{path: '/', redirect: '/musichall'},
{path: '/musichall', component: Musichall,
children: [
{path: 'playlist', component: Playlist},//注意子路由不用加'/'
{path: 'singers', component: Singers},
]
},
{path: '/mymusic', component: Mymusic},
]
})
1.7 动态路由
假设有如下图的几行歌手,点进去都是每个歌手各自的详情页面,但我们不可能为每一个歌手都创建一个歌手详情组件页面。而是重用组件的模板页面,当点击一个歌手时,从后台获取相应的数据,然后在传给组件中的属性,最终显示出来。
动态路由是指:把Hash地址中可以改变的部分定义为参数项,从而提高路由规则的复用性。在vue-router中使用英文的冒号(:)来定义路由的参数项(Musichall->Playlist->Detail三级路由):
{path: '/musichall',
component: Musichall,
children: [
{path: 'playlist', component: Playlist},
{path: 'singers',
component: Singers,
children: [
{path: ':id', component: Detail}
]
},
]
},
通过动态路由匹配的方式渲染出来的组件中,可以使用$router-.params对象访问到动态匹配的参数项,比如在歌手详情组件内部页面根据id值,请求不同的歌手数据:
<template>
<h1>歌手详情页{{ $route.params.id }}</h1>
</template>
当然可以不通过$route.params.id获取参数,vue-router允许在路由规则中开启props传参,在组件内部定义属性:
<template>
<h1>歌手详情页{{ id }}</h1>
</template>
<script>
export default{
name: 'Detail',
props: [":id"],
}
</script>
然后在路由映射中说明id以属性传递给组件:
{path: ':id', component: Detail, props: true}
1.8 编程式路由导航
声明式 | 编程式 |
---|---|
<router-link to="..."><router-link> | router.push("...") |
除了使用<router-link>创建a标签来定义导航链接,还可以借助router的实例方法,通过编写代码来实现。
<template>
<div>
<h2>万千歌手,尽在眼前</h2>
<button @click="gotoSingerDetail(1)">跳转到歌手1</button>
<router-view></router-view>
</div>
</template>
<script>
export default{
methods: {
gotoSingerDetail(id){
this.$router.push("/musichall/singers/${id}")
}
}
}
</script>
想要导航到不同的路由(URL),则使用router.push("...")方法。这个方法会向history栈添加加一个新的记录,所以当用户点击浏览后后退按钮时,会回到之前的URL。
当点击<router-link>时,这个方法会在内部调用,所以说点击<router-link>等同于调用router.push("...")
1.9 导航守卫(拦截器)
路由守卫可以控制路由的访问权限,类似于后端拦截器。
全局路由导航守卫会拦截每一个路由,从而对每个路由进行访问权限的控制。可以在路由配置模块中使用router.beforeEach注册一个全局前置导航守卫:
router.beforeEach( (to, from, next)=>{
if(to.path==='/main' && !isAuthenticated){//to: 即将进入的目标, from: 当前路由
next('/login')
}
else{
next()//在参数中声明next形参后必须调用next()函数,否则从当前路由不允许访问任意路由
//next():直接放行;next(false):强制停留在当前路由;next('/login'):强行跳转。
}
})
2 状态管理Vuex
Vuex状态管理一般用在复杂的大型项目中才会用到,也就说在一般的网站中Vuex不是必须的。
对于组件化开发来说,大型应用的状态往往跨越多个组件。在多层嵌套的父子组件之间传递数据状态已经十分麻烦,而Vue更是没有为兄弟组件提供直接共享数据的办法。基于这个问题,许多框架提供了解决方案——使得全局的状态管理器,将所有分散的共享数据交由状态管理器保管,Vue框架也是这样。
Vuex就是一个专门为Vue.js应用程序开发的状态管理库,采用集中式存储管理应用的所有组件的状态。简单地说,Vuex用于分发和管理Vue各个组件中的数据。
Vue2和Vue3分别对应VueX3和VueX4版本
安装:npm install vuex@next,安装next的最新版本
每一个Vuex应用的核心都是一个全局实例store,与普通的全局对象不同的是,基于Vue数据与视图绑定的特点,当store中的数据状态发送变化时,与之绑定的视图也会被重新渲染。store中的状态不允许被直接修改,改变store中的状态的唯一途径就是显式地提交mutation,这样可以让我们方便地跟踪每一个状态的变化。
在大型复杂项目中,如果无法有效地跟踪到状态的变化,将会对理解和维护代码带来极大的困扰。
Vuex中有五个重要的概念:State、Getter、Mutation、Action、Module。
store中state的数据发送变化时可以被重新渲染到Vue组件中,或者当Vue组件发送网络的异步请求时,会通过dispatch派遣的方式去触发action发送异步请求获取数据,然后通过提交Mutation中的方法将对应的数据修改为新的数据,实现对state数据状态的改变,最终渲染到Vue组件页面中。
2.1 State与Mutation和Action
State用于维护所有应用层的数据状态,并确保应用只有唯一的数据源;Mutation中定义数据修改方法;Action类似于Mutation;Getter用来进一步监测维护由State派生的一些数据状态。
首先我们在项目中store文件夹下创建一个js文件,导入vuex后显示地通过Vue.use(Vuex)来安装Vuex,安装之后就可以创建store了:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex)
const store = new Vuex.Store({//Vue2(VueX3):new Vuex.Store
state (){
return {
//所有的数据都可以定义到state中去共享
count: 2,
todos: [
{id: 1, name: "学习", done: true}, //待办事项已完成
{id: 2, name: "运动", done: true}, //待办事项未完成
{id: 3, name: "吃饭", done: false},
{id: 4, name: "睡觉", done: false}
]
}
},
mutations: {
increment (state, n){//可以加n载荷也可以不加,在组件中直接提交
state.count+=n //基于Mutation在其中修改数据状态,外部只要调用increment()方法就可以对 store中的数据状态state进行修改。
}
},
actions: {
increment (context){
context.commit('increment');
}
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done) //过滤出所有已完成的并返回
}
}
})
export default store //导出,然后还需要在入口文件main.js导入并注册到vue对象中,方便任意组件使用store取数据
在模块化的构建系统中,在每个需要使用state的组件中需要频繁地导入。Vuex通过store选项,提供了一种机制将store从根组件"注入"到每个子组件中,在入口文件中从上面写好的js文件导入和注册Vuex对象store:
import Vue from 'vue'
import App from './App.vue'
import store from './store/index.js'
new Vue({
render: h => h(App),
store: store
}).$mount('#app')
这样在任意组件中就可以利用store对象获得state:
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
2.2 在组件中改变和获得store的状态
提交Mutation中的方法改变store的状态,在main.js文件中导入和注册store对象后,组件中就可以直接使用this.$store.commit("...")显式地提交mutation改变store中的状态,或者通过mapMutations映射的方式隐式提交,然后在计算属性中直接使用this.$store.state.count或者通过mapState映射的方式:
<template>
<div>
<!-- {{this.$store.state.count}} -->
<!-- 以上数据变化可以通过计算属性会监听并返回 -->
{{count}}
<button @click="plus(2)"> +2 </button><!-- plus方法中通过显示提交,定义plus和提交时需要显示传递参数 -->
<button @click="add(4)"> +4 </button> <!-- 在add通过映射的方式提交,定义add时会自动传递 -->
<!-- mapState -->
<ul>
<li v-for="todo in todos" :key="todo.id">事项{{todo.id}}{{todo.name}}</li>
</ul>
<!-- mapGetters获取已完成事项 -->
<ul>
<li v-for="todo in doneTodos" :key="doneTodos.id">已完成事项{{todo.id}}{{doneTodos.name}}</li>
</ul>
</div>
</template>
<script>
import {mapState, mapGetters, mapMutations} from 'vuex'
export default {
//在vue的计算属性定义映射方法,监听并返回某个数据状态,可以不用data属性返回
computed: {
count(){
return this.$store.state.count
},
//可以通过mapState辅助函数获取多个数据状态
//mapState已经做导入,不用通过this.$store.mapState调用
...mapState([ //"..."表示使用对象展开运算符将 State 混入 computed 对象中
//通过属性传递简化写法mapState([])
'count',
'todos',
//通过将方法传递到对象中mapState({})获取状态
//count: state => state.count,
//todos: state => state.todos,
//为了使用'this'获取局部状态,必须使用常规方法,通过将方法传递到对象中mapState({})获取
//countPlusLocalState(state){
// return state.count + this.localCount
//}
]),
//可以提供通过mapGetter辅助函数获取多个state中已完成事项
//mapGetter已经做导入,不用通过this.$store.mapGetter调用
...mapGetters([// 使用对象展开运算符将 getter方法 混入 computed 对象中
'doneTodos',
])
},
methods: {
//在普通函数中提交store的状态state,改变store中状态state的唯一途径就是显式地提交mutation
plus(n){
this.$store.commit("increment", n) //提交时可以传入数据,数字,字符串或者对象
},
//可以在mapMutations辅助函数中的定义多个方法映射提交修改对应数据状态的方法。
//通过提交Mutation中的方法修改store中的状态state
...mapMutations({ //"..."表示使用对象展开运算符将 Mutation 混入 methods 对象中
add: 'increment', //将this.add()映射为this.$store.commit('increment')
//'incrementBy', //将this.incrementBy()映射为this.$store.commit('incrementBy'),通过属性传递mapMutations(['incrementBy'])
})
}
}
</script>
定义组件路由,启动显示:
Action类似Mutation,不同在于:
Action不是直接改变状态,而是通过上下文对象提交mutation来间接地修改state中的数据状态,context中可以记录操作,Action可以包含异步操作。
在组件中,可以通过使用this.$store.dispatch('xxx')触发action中的'increment(context)'方法,或者使用mapActions辅助函数先将其映射下来。
methods: { increment(){ this.$store.dispatch('increment'); //或者 mapActions([ 'increment', //将'this.increment()'映射为'this.$store.dispatch('increment')' ]) } }
在组件中,调用Getter中的方法可以直接使用this.$store.getter.doneTodos:
methods: {
add(){
this.$store.commit('increment'); //通过提交Mutation中的方法将对应的数据修改为新的数据
console.log(this.$store.state.count);
},
getter(){
this.$store.getter.doneTodos
}
}
2.3 Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store对象就有可能变得相当臃肿。
为了解决以上问题,Vuex允许我们将store分割成模块(modules)。每个模块拥有自己的state、mutation、action、getter,最后在创建store对象时在modules属性中映射模块,该模块就称为了store对象的state:
const moduleA = {
state: () => ({...}),
mutations: {...},
actions: {...},
getters: {...}
}
const moduleB = {
state: () => ({...}),
mutations: {...},
actions: {...},
getters: {...}
}
const store = createStore({//Vue2(VueX3):new Vuex.Store
modules: {
a: moduleA,
b: moduelB
}
})
//store.state.a => moduleA.state
//store.state.b => moduleB.state
3 MockJS前端数据模拟
3.1 基本使用
Mock.JS是一款前端开发中拦截Ajax请求再生成随机数据响应的工具,可以用来模拟服务器的数据响应。优点是简单方便,无侵入性,基本覆盖常用的接口数据类型。支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等。MockJS只是在前端的开发阶段会使用,当后端接口开发完成后只需移除即可,方便于前后端独立开发。
安装:npm install mockjs
在项目中创建mock文件夹,新建js文件,导入Mock,通过Mock.mock('url', data )方法生成数据,数据生成规范包含两层规范,数据模板(DTD)、数据占位符(DPD较常用):
//引入mockjs
import Mock from 'mockjs'
//使用mockjs模拟数据
//第一个参数为对指定请求路由(url)的拦截,如果拦截的请求中传递有参数,/musichall/singers?id=1,或通过设置props: true传递属性。拦截的url可以通过正则表达式对应请求的url。".*": 表示任意字符。
Mock.mock(RegExp('/musichall/singers.*'), {
"ret": 0,
"data": {
//通过占位符随机生成日期和中文名字,占位符只是在属性值字符串中占个位置,并不出现在最终的属性值中
//当我们使用@datetime这样的占位符时会调用Mock.Romdom生成随机数据的工具类中的方法
"mtime": "@datetime",
"nickname": "@cname",
//通过数据模板随机生成1-10的数字,'1'仅代表生成数据的类型
"score|1-10": 1,
//生成图片,长宽、背景颜色、文字颜色、图片类型、图片中的文本
"img": "@image('200x200', '#ffcc33', '#FFF', 'png', '生成图片')" //注意乘号
}
})
然后在入口文件main.js中导入,当后端接口开发完成后只需移除即可:
import './mock'
组件中通过axios发送异步请求,然后会调用mockjs中模拟的数据接口进行拦截,这时返回的response中的data就是mockjs中用Mock.mock('url', data)中生成的ret和data:
<template>
<div>
<!-- 获取动态的id值 -->
<h1>歌手详情{{ id }}</h1>
<img alt="singer" :src="img">
</div>
</template>
<script>
export default{
props: ["id"],
created: function(){
this.$http.get("/user/findAll").then( (response) => {
console.log(response);
console.log(response.data.data.img);
this.img = response.data.data.img
})
},
data: function(){
return {
img: ""
}
}
}
</script>
获取显示成功:
4 Vue-Element-Admin
4.1 下载和安装
Vue-Element-Admin是一个后台前端管理解决方案,它基于vue和element-ui实现。内置i18国际化解决方案,动态路由,权限认证,提炼了典型的业务模型,提供了丰富的功能组件。可以快速搭建企业级中后台产品原型。
Vue-Element-Admin是后台集成方案,不太适合当基础模板来进行二次开发因为可能集成了很多自己用不到的功能,造成不少代码冗余。如果项目简单可以基于vue-element-template基础模板开发。
因为该项目开源在Github和Gitee上,可以通过Git工具进行克隆
git clone https://github.com/PanJiaChen/vue-element-admin.git #克隆集成模板
git clone https://gitee.com/panjiachen/vue-admin-template.git #克隆基础模板
#进入项目目录
cd vue-element-admin
#安装依赖
...
或者通过vscode打开,然后进行依赖的安装:
# 安装依赖,可能会安装失败
npm install
# 建议不要用 cnpm 安装 会有各种诡异的bug 可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npmmirror.com
注意,Windows安装依赖不成功,是因为项目中开发阶段使用的工具中包含sass,就像前面使用VueCli脚手架构建项目时,取消选择的eslint开发阶段工具。因为 node-sass 是依赖 python2的环境的,如果之前没有安装和配置过的话,sass会安装失败,需要自行安装配置一下。
该模板项目也用到了eslint,如果不想使用eslint校验(不推荐取消),只在vue.config.js文件设置以下属性即可:
lintOnSave: false
4.2 启动项目
在package.json文件中的JavaScript脚本属性值"vue-cli-service serve"的属性名改为了"dev",也就启动时将npm run serve改为npm run dev即可。
npm run dev
另外,通过浏览器访问该项目默认端口是9527,要切换可以在模块导出中的devServer修改port属性。devServer用来在系统中配置我们的开发环境,只是用于本机开发和调试,一般打包发布之后就用不到了。
4.3 项目的理解
在src目录下,包含components、views、router、store等几个重要的目录。
router目录,也就是用于设定访问路径,并将路径和组件映射起来,实现在页面中进行组件的切换和显示。其中index.js文件用于设定组件和子组件的访问路径,将路径和组件映射关联起来。可以说路由就是组件。
其次是components和views目录,其中关于页面相关的组件都是放在views下,而components下一般放一些通用(可复用)的组件。在组件中可以提交改变相关状态dispatch方法。
store目录,也就是前面说的Vuex状态管理的核心,组件中的数据状态都由全局实例store管理。在Index.js文件中创建store对象时在modules属性中映射了app、settings、user三个模块,管理着各自模块状态的改变。在组件中调用dispatch派遣的方式触发和传递相关数据给store中的actions方法和,在actions中调用相应的网络请求接口并传递数据。
api目录中的js文件就是封装了相应aixios网络请求接口,用来接收参数包括路由、请求方法、数据。
最后在utils目录中的request.js文件更具体地进行axios网络请求。所以一切的开头是组件,然后通过Vuex的store一层层的改变状态,最后在utils中向服务端发送请求。
除以上之外,还有layout目录中做相关的组件布局,styles目录中放样式文件(scss),assets目录中放了一些图片,icons目录中放了一些图标。
以登录过程为例:
所有的路由跳转都会先经过导航守卫:
第一,如果有token且想请求登录页面,那就不用登录了直接进入首页。如果有token而请求其他页面(如进入首页点击进入其他组件页面),且如果存在用户信息,那么直接进入请求的页面,没有就发送请求获取后再进入。
第二,如果没有token且想请求白名单页面,那放行进入,如登录页,可以输入账号密码后发送异步请求传递账号和密码获取响应response数据,并存储token再进入首页。(如果没有token想请求非白名单页面,则先进入登录页登录后再重定向到想去的页面)。
以下就是没有token进入登录页的用户登录过程:
用户输入账号密码,前端通过双向绑定初步校验通过后提交信息触发方法执行dispatch派遣action发送请求。
action中通过调用api目录下的网络请求接口发送异步请求传递账号和密码,获取后端响应response数据(data),从响应中拿到token并提交mutation记录到state状态中(存到了内存中)。除此之还通过setToken(data.token)存到了本地中。然后跳转到首页。
其他信息的请求接收到响应数据data后:
acton中提交mutation修改state数据状态
getter会获取store的最新状态,并渲染到组件页面中。
5 打包部署
5.1 打包
执行NPM脚本程序build:
或者直接打开终端:npm run build
已经成功打包在dist目录下:
5.2 部署
Nginx是一款轻量级的wen服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,在各大型互联网公司都有非常广泛的使用。
进入官网下载软件包后解压:
conf:配置文件目录 html:静态资源文件目录 logs:日志文件目录 temp:临时文件目录 nginx.exe:nginx可执行文件
部署静态资源:
部署:将打包好的dist目录下的文件全部复制到nginx安装目录的html目录下。
启动:双击nginx.exe文件即可,Nginx服务器默认占用80端口号。如果80端口号被系统占用(netstat-ano|findStr 80),可以在nginx.conf中修改端口号为xxx。
当然Nginx功能非常强大,这里只是演示一下在Nginx上部署静态资源,后面还学通过Nginx怎么做反向代理,如何实现负载均衡。