前言

公司之前一直都是单体应用开发,从今年的项目开始逐渐开始向前后端分离的模式中转变。前端项目偏向于单体前端应用(基于vue脚手架的模式),以往开发前端都只是会写页面会写组件,对前端框架没有过多的了解,纯粹的停留在会用的情况下。为此准备系统的学习一下前端的项目结构,因此选择了B站上面的一个和平常项目技术栈比较相似的课程(vue + webpack + vuex + vue-router + axios + less),附上课程连接。这篇文章旨在可以让后端开发不用看视频课程也能大致了解前端开发的架构,开发模式等。同时也算是一个课程学习笔记,以提供日后快速回顾课程内容。

本篇笔记针对快速搭建前端项目,以及对项目结构的一些说明,不涉及业务如何实现等具体细节。适用于有接触过前端但是不太清楚纯前端开发的流程,框架之类的同学,以及后端刚接触前端框架的同学。

项目初始化

准备工作

  • node.js
  • npm
  • @vue/cli

创建初始项目

  1. 安装好ndoe.jsnpm

  2. 执行下面命令安装@vue/cli

    1
    2
    3
    npm install -g @vue/cli
    ##安装完毕后执行下面命令检查安装是否正常
    vue --version
  3. 创建最基础的项目

    1
    2
    3
    4
    #方案1 直接执行vue create 命令 在命令行创建
    vue create <项目名称>
    #方案2 执行vue ui 命令,在图形化页面创建
    vue ui

    image-20211231131433655

    无论是从图像化或者是命令行创建都是一样的,可选择通用项目架构模板,也可以根据自己的需要加入各种模块依赖。我这里创建的基本项目包含了

    image-20211231132107353

    创建完了如果需要加入新的依赖可以直接通过图形化添加,或者在命令行 npm install xxx新增。

    如果想进一步了解@vue/cli,请参考vue-cli官方说明文档

项目结构说明

项目基础模块如下图:

image-20211231134428112

  • dist: 项目打包后npm run build的参物,用于正式环境部署
  • node_modules: 项目依赖,npm install的产物
  • public: 存放公共静态资源,webpack打包会原封不动的打进dist中
  • assets: 组件间的公用静态资源,webpack打包会将这些资源打包成一个模块到js里。
  • components: 非路由组件(全局组件)
  • router: 路由配置目录
  • store: vuex状态管理目录
  • views: 路由组件,也就是平常我们写的跳转页面。
  • App.vue: 唯一根目录,Vue当中的组件
  • main.js: 程序入口文件,也是程序最先执行的文件
  • .gitignore: git提交忽略文件
  • balel.config.js: babel相关配置文件
  • package.json: 项目依赖版本,配置等相关信息
  • package-lock.json: 本地缓存
  • README.md: 项目说明文档

其他基础配置

启动开发环境自动打开浏览器
修改package.json文件,加入--open启动参数即可

1
2
3
4
5
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
}

关闭eslint校验功能

package.json同级目录下新建vue.config.js文件,添加如下配置

1
2
3
module.exports = {
lintOnSave:false
}

但是个人不建议关闭,这个虽然有点烦,但是可以让代码更加规范。

设置src别名

package.json同级目录下新建jsconfig.json文件,添加如下配置:

1
2
3
4
5
6
7
8
9
{
"compilerOptions":{
"baseUrl": "./",
"paths": {
"@/*":["src/*"]
},
"exclude":["node_modules","dist"]
}
}

这样就可以用@来代替src的目录,可以这样定位资源了,告别烦人的../../了。

1
2
3
import HelloWorld from '@/components/HelloWorld.vue'

<img src="@/assets/logo.png" />

新版脚手架创建的项目,不加入jsconfig.json这个配置,就可以使用别名了,因为底层实现了,实现方法node_modules/@vue/cli-service/lib/config/base.js

image-20211231144640016

项目相关命令

1
2
3
4
5
6
7
8
#安装依赖
npm install
#热部署开发
npm run serve
#打包到dist给正式环境
npm run build
#检验
npm run lint

路由(Vue-router)

安装

  • 之前创建项目的时候选择了vue-router,则创建出来的项目就已经整合了vue-router模块,直接使用,无需配置

  • 之前没有或忘记配置了,可以根据以下步骤安装路由

    1. 安装模块
    1
    npm install vue-router
    1. 创建router文件夹,创建index.js引用路由

    image-20211231150212783

    1. main.js中注册暴露的路由
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router' //引入路由配置
    import store from './store'

    Vue.config.productionTip = false

    new Vue({
    //注册路由,key value一样,省略value
    router,
    store,
    render: h => h(App)
    }).$mount('#app')
    1. App.vue中配置路由展示,<route-view/>标签

    image-20211231154432577

相关变量

  • $route: 一般用于获取路由信息【路径、query、params等】
  • $router: 一般用于编程式导航进行路由跳转【push|replace】

路由跳转配置

跳转方式

  • **<router-link>**:声明式跳转

    1
    <router-link to="/about">跳转连接</router-link>
  • $router:编程式导航

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 字符串
    router.push('home')

    // 对象
    router.push({ path: 'home' })

    // 命名的路由 /user/123
    router.push({ name: 'About', params: { userId: '123' }})

    // 带查询参数,变成 /register?plan=private
    router.push({ path: 'register', query: { plan: 'private' }})

    如果需要params在url中展示,则需要在路由上面编写占位符,如:

    1
    2
    3
    4
    5
    {
    path: '/about/:userId',
    name: 'About',
    component: () => import('../views/About.vue')
    }

    这样路由跳转的地址为 xxx/about/123

全局404路由配置

1
2
3
4
5
{
path:'*',
name:'notFound',
component: notFound
},

通过通配符匹配设置404页面统一跳转。

Props

在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。

使用 props 将组件和路由解耦的三种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
path: '/',
name: 'Home',
component: Home,
props:true //这个设置成true,则route.params会设置成组件的属性
},
{
path: '/',
name: 'Home',
component: Home,
props: {name:'tangseng',age:18} //对象会设置到组件的props中
},
{
path: '/',
name: 'Home',
component: Home,
props: route => ({ query: route.query.q }) //对象会设置到组件的props中
},

meta

路由元数据,可以通过$route获取到各个路由的meta信息

1
2
3
4
5
6
7
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
meta:{
isShow:false
}

跳转到该页面后

image-20211231163818247

懒加载

当页面访问到的时候才加载,懒加载路由配置

1
2
3
4
5
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
}

把组件位置写成动态Import即可()=>import()

其中中间的单行注释是webpackChunkName是按组分块,Webpack会将任何一个异步模块与相同的块名称组合到相同的异步块中。

进一步了解Vue-router,请参考Vue Router 官方说明文档

其他

不断跳转url地址不变的路由,会报Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current location异常。

页面代码:(自己跳转自己)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div class="about">
<h1>This is an about page</h1>
<button @click="fun">点击跳转</button>
</div>
</template>
<script>

export default {
name: 'about',
data(){
return{
}
},
components: {
},
mounted() {

},
methods:{
fun() {
this.$router.push("About")
}
}
}
</script>

image-20211231165920595

解决:

  • 不影响使用,无视即可

  • push的时候传入成功和中止的回调

    1
    2
    3
    fun() {
    this.$router.push("About",()=>{},()=>{})
    }
  • 底层修改(增强push方法)

    router/index.js添加对push方法的增强:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //保存原始push方法
    let originPush = VueRouter.prototype.push;
    //重写push方法
    VueRouter.prototype.push = function(location,onComplete,onAbort){
    //成功跳转、中止跳转回调都传入,调用原来的方法
    if(onComplete && onAbort){
    originPush.call(this,location,onComplete,onAbort);
    }else if(onComplete){
    //只传入成功跳转,则传入成功跳转,构造中止跳转
    originPush.call(this,location,onComplete,()=>{});
    }else{
    //都不传则都构造回调函数
    originPush.call(this,location,()=>{},()=>{});
    }
    }

axios

axios基于promise网络请求库,作用于node.js和浏览器中。在服务端它使用原生的node.js http模块,而在客户端(浏览器)则使用XMLHttpRequests。简单来说就是用来发送请求的,我们一般用他的请求拦截器和响应拦截器来对我们发送的请求进行封装。详情参考Axios官方文档

安装

1
npm install axios

直接使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//引入axios
import axios from 'axios';

//在方法里请求
methods: {
fun() {
//axios(config) 请求
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
},
fun2() {
//POST别名请求 axios.post(url[,data][,config])请求
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
}).then(function (response) {
console.log(response);
}).catch(function (error) {
console.log(error);
});
}
}

具体还有哪些别名请求,config参数说明等,可以参考axios官方文档

二次封装

项目中通常创建一个api的文件夹,里面创建一个request.js利用axios的请求拦截器和响应拦截器对axios进行二次封装。

简单的使用请求拦截器和响应拦截器对axios封装的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import axios from 'axios';

let instance = axios.create({
//超时时间
timeout: 5000
});

// 请求拦截器
instance.interceptors.request.use(
config => {
//请求拦截器,可以对config进行统一的修改,如Header
config.headers.userId = "myUserId";
console.log("do something before request")
return config;
}, error => {
//
Promise.reject(error);
})

// 响应拦截器
instance.interceptors.response.use(
response => {
//响应拦截器
const res = response.data
//如在这里判断 code 值,返回data
if (res.code == '200') {
return res;
} else {
//简单例子
alert("请求失败," + res.message);
return res;
}
}, () => {
//响应失败
alert('系统忙,请稍后再试')
Promise.reject(new Error("响应失败"))
});

export default instance;

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import axios from '@/api/request' //引入封装的axios
export default {
name: 'about',
data() {
return {}
},
components: {},
mounted() {

},
methods: {
fun() {
//发送请求
axios.get('/data.json');
}
}

API接口统一管理

在大型前端项目中,接口复用情况可能比较多,所以需要对接口进行统一管理;

一般情况下,在api目录下创建index.js在里面将接口以方法的方式暴露出去。

举个栗子:

@api/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//统一管理API
import axios from './request';

//获取用户接口
export const getUser = () =>{
//axios 发送请求返回Promise对象
return axios({
url:'/getUser',
method:'get'
})
}

//简写
export const getUser2 = ()=> axios({url:'/getUser2',method:'get'});

页面调用接口:

user.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
//引入函数
import {getUser2} from "@/api";
export default {
name: 'about',
data() {
return {}
},
mounted() {
//调用函数发送请求
getUser2();
},
methods: {

}
}
</script>

代理(devServer)

开发的时候处理跨域问题,直接用代理即可。vue.config.js或者webpack.config.js加入配置,官方参考文档

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
//...其他配置
//代理配置
devServer: {
proxy: {
//将所有以api开头的,都代理到 http://localhost:3000/api
'/api': {
target: 'http://localhost:3000'
},
},
},
};

添加完配置后,重启服务npm run serve即可。

vuex

vuexvue官方提供的插件,集中管理组件共用的数据。详情参考vuex官方文档

当众多组件之间都共用同一公共数据(共享同一状态)时,可以用vuex把状态存起来,方便组件之间调用。如权限控制中Role角色很多页面的组件都会使用到,就可以将他用vuex存储起来,供其他组件使用,某一组件将其状态改变,其他用到这个role的值都会同步更新(共享状态)。

安装

  1. 安装vuex依赖
1
npm install vuex --save
  1. 新建src/store文件夹
  2. src/stroe文件夹中新建index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'    //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //使用一次vuex
//对外暴露vuex.store
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})
  1. 在程序入口main.js注册 store
1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
router,
store, //注册sotre,之后就可以使用 this.$store 了
render: h => h(App)
}).$mount('#app')

相关概念

  • state:单一状态树。一个对象就包含了全部的应用层级状态,把他当成一个定义状态,存放状态的仓库吧。
  • Mutation: 更改Vuex的store中的状态的唯一方法是提交mutation,就是定义一个函数,函数里面可以修改state里面的值(修改状态),通过store.commit(函数名)来触发函数修改状态。
  • ActionsAction提交的是mutation,间接通过mutation改变状态。通过store.dispatch(action函数名) mutation必须同步执行,Action不收约束,可以同步也可以异步。如果需要异步修改状态,则在action`内部执行异步操作即可。
  • Getter: state里面是存放状态的对象,对象里面有有各自的状态,可以用getter来过滤,根据状态(对象)里面的状态筛选获取符合的对象(状态)
  • Module: Vuex允许我们将store分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。也就是每个小store组成一个store

例子:

  1. 模拟登录后,将权限角色存储到vuex的store中。同步
  2. 模拟退出登录5秒后,将vuex的store的角色清除,异步。

src/store/index.js定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
ROLE:''
},
//只能通过 mutation 对 state的值进行修改
mutations: {
//设置角色
SET_ROLE(state,role){
state.ROLE = role;
},
//清除角色
CLEAR_ROLE(state){
state.ROLE = ''
}
},
//action 是执行 mutation的
actions: {
//异步清除角色
CLEAR_ROLE_TIME_OUT(context){
setTimeout(()=>{
//执行CLEAR_ROLE mutation
context.commit("CLEAR_ROLE")
},3000)
}
},
modules: {

}
})

demo.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div class="home">
<div>用户角色为{{role}}</div>
<button @click="login">登录</button>
<button @click="logout">退出登录</button>
</div>
</template>

<script>

export default {
name: 'Home',
components: {
},
mounted() {

},
//用计算属性获取store里面的状态
computed:{
role(){
return this.$store.state.ROLE;
}
},
methods:{
login(){
//.....登录成功了设置角色
let role = '0001';
//通过提交mutation来改变状态
this.$store.commit('SET_ROLE',role);
},
logout(){
//退出登录,异步清除共享状态(role)
//使用Action分发,来异步执行更新状态
this.$store.dispatch('CLEAR_ROLE_TIME_OUT')
}
}
}
</script>

效果

image-20220105174706895

PS:上面的vue-router、axios、vuex等可以在项目初始化的时候直接选择需要的依赖,在初始化完毕后,已经配置好了,直接使用即可。

性能优化

防抖

连续触发函数,最后一次执行在规定时间之后才会触发,快速执行仅仅会执行一次。用户操作很频繁,只执行一次。

节流

在规定间隔时间范围内不会重复触发回调,将频繁的触发变为少量的触发。(如计数器,点击按钮加一,可以用节流函数规定,N秒内无论点击多少次,只执行一次)

用户操作很频繁,在规定时间范围执行一次

节流和防抖都可以通过引入lodash来实现,也可以自己写js实现(闭包+延时器)

扩展资料