1.vuex
数据共享问题
- 定义全局的window对象,然后使数据有响应式,用ref reactive等
- vuex全局维护所有组件状态,同时自带响应式
例子
store/index.js
import {createStore} from 'vuex'
const store = createStore({
state(){
return {
cnt:111
}
},
mutations: {
addCnt(state){
state.cnt++
}
}
})
export default store
test.vue
<template>
<div>
<button @click="test">{{store.state.cnt}}</button>
</div>
</template>
<script setup lang="ts">
import {useStore} from 'vuex'
const store = useStore()
function test(){
store.commit("addCnt")
}
</script>
<style scoped>
</style>
源码实现
vuexMini.js
import {inject,provide,reactive,computed} from 'vue'
const storeKey = '__store__key__'
function useStore(){
return inject(storeKey)//这里返回注入的 store
}
function createStore(options){
return new Store(options)
}
class Store{
constructor(options){
this._state = reactive({
data:options.state() //这里需要主动调用一次state才能返回数据
})
this._mutations = options.mutations
this._actions = options.actions
this.getters = {}
const kes = Object.keys(options.getters)
for (let index = 0; index < kes.length; index++) {
const key = kes[index];
const fun = options.getters[key]
Object.defineProperty(this.getters,key,{
get: ()=>{
return computed(()=>{ //设置缓存
return fun(this)
}
)
}
})
}
}
install(app) {//注册时候的代码,把store挂载到全局
app.provide(storeKey,this)
}
get state(){
return this._state.data // 这里只返回内部变量的data
}
commit(key,palyload){
this._mutations[key](this,palyload)
}
dispatch(key,palyload){
this._actions[key](this,palyload)
}
}
export {createStore, useStore}
store/index.js
import {createStore} from '../vuexMini.js'
const store = createStore({
state(){
return {
cnt:111
}
},
mutations: {
addCnt(store){
store.state.cnt++
}
},
getters: {
doubleCnt(store){
return store.state.cnt * 2
}
},
actions: {
addCntByReq( store){
new Promise( (resolve,reject)=> {
setTimeout(() => {
resolve()
}, 1000);
}).then((res) => {
store.commit("addCnt",100)
})
}
}
})
export default store
test.vue
<template>
<div>
{{store.state.cnt}}
{{store.getters.doubleCnt}}
<button @click="test">xxx</button>
</div>
</template>
<script setup lang="ts">
import {useStore} from '../vuexMini.js'
const store = useStore()
function test(){
store.commit("addCnt",store.state.cnt++)
store.dispatch('addCntByReq')
}
</script>
<style scoped>
</style>
pinia
- 解决vuex中对ts 类型支持差问题
2.vue-router
2种方式
- 传统jsp ,后端决定路由与页面数据,每次都是整页刷新
- 前端js控制本地路由与url变化,实现内容局部刷新交互: 早在jquery时代 pushState+ajax 就已经流行
前端路由方式
- hash
- 通过#锚点实现
- hashChange监听变化
- history
- 通过history.pushState,replaceState
- history.popState监听变化
源码实现
vueRouterMini.js
import {ref,inject} from 'vue'
import RouterLink from './RouterLink.vue'
import RouterView from './RouterView.vue'
const ROUTER_KEY = '__router__'
function createRouter(options){
return new Router(options)
}
function useRouter(){
return inject(ROUTER_KEY)
}
function createWebHashHistory(){//构造方法 返回 {bindEvents:xx, url:xxx}
function bindEvents(fn){
window.addEventListener('hashchange',fn)
}
return {
bindEvents,
url:window.location.hash.slice(1) || '/'
}
// 等价于
// this.current = ref("/")
// window.addEventListener('hashchange', () => {
// this.current.value = window.location.hash.slice(1)|| '/'
// })
}
class Router{
constructor(options){
this.history = options.history
this.current = ref(this.history.url)
this.history.bindEvents(()=>{
this.current.value = window.location.hash.slice(1)
})
this.routes = options.routes
}
install(app){
app.provide(ROUTER_KEY,this)
app.component("router-link",RouterLink)
app.component("router-view",RouterView)
}
}
export {createRouter,createWebHashHistory,useRouter}
RouterLink.vue
<template>
<a :href="'#'+props.to">
<slot />
</a>
</template>
<script setup>
import {defineProps} from 'vue'
let props = defineProps({
to:{type:String,required:true}
})
</script>
RouterView.vue
<template>
<component :is="comp"></component>
</template>
<script setup>
import {computed } from 'vue'
import { useRouter } from './routerMini.js'
let router = useRouter()
const comp = computed(()=>{
const route = router.routes.find(
(route) => route.path === router.current.value
)
return route?route.component : null
})
</script>
router/index.js
import {
createRouter,
createWebHashHistory,
} from './routerMini'
import Home from '../pages/home.vue'
import About from '../pages/about.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
3.性能测试
- lighthouse灯塔 选择performance分析,显示各个性能指标, 并根据建议优化
4.jsx
temlpate
- 缺点
- 功能单一
- 不支持动态的需求
- 优点
- 在vue3里会做很多编译时候的性能优化
- 把重复的数据做提升
- 事件缓存
- 静态节点标记,优化diff效率
- 使用简单
- 在vue3里会做很多编译时候的性能优化
vue组件的渲染流程
- template -> h函数 -> vnode
- 使用过于复杂
h函数
使用
<template>
<div>
<myhead :level="2">标题头1111</myhead>
</div>
</template>
<script setup lang="ts">
import myhead from './myhead.jsx'
</script>
myhead.jsx
import { defineComponent, h } from 'vue'
export default defineComponent({
props: {
level: {
type: Number,
required: true
}
},
setup(props, info) {
return () => h(
'h' + props.level, // 标签名
{}, // prop 或 attribute
info.slots // 子节点
)
}
})
jsx
jsx是js的一种语法糖
参考
const el = <h1 class="cls">test11</h1>
//等价于
const el = createVnode('h1',{class:"cls"}, 'test11')
myhead.jsx
import { defineComponent, h } from 'vue'
export default defineComponent({
props: {
level: {
type: Number,
required: true
}
},
setup(props, info) {
const tag = 'h'+props.level
console.log(tag,info.slots.default)
return () => <tag>{info.slots.default()}</tag>;
}
})
- 特点
- 支持更动态需求
- 支持返回不同的组件
export const Button = (props,{slots})=><button {...props}>slots.default()</button>
export const Input = (props)=><input {...props} />
- 缺点
- 失去vue3的性能优化
- 维护麻烦
配置
安装
npm install @vitejs/plugin-vue-jsx -D
设置 vite.config.js
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
plugins: [vue(),vueJsx()],
})
5.实战优化
1. Vue 项目的规范和基础库封装
配置
npm install element3 --save
npm i axios -D
npm install -D sass
axios
import axios from 'axios'
import { useMsgbox, Message } from 'element3'
import store from '@/store'
import { getToken } from '@/utils/auth'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
timeout: 5000, // request timeout
})
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['X-Token'] = getToken()
}
return config
},
error => {
console.log(error) // for debug
return Promise.reject(error)
},
)
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 20000) {
console.log('接口信息报错',res.message)
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
console.log('接口信息报错' + error)
return Promise.reject(error)
},
)
export default service
eslint
npm i eslint -D
npx eslint --init进行配置
package.json
"scripts": { "lint": "eslint src/** --fix" }
husky
npm install -D husky # 安装husky
npx husky install # 初始化husky
npx husky add .husky/commit-msg "node scripts/verifyCommit.js"
scripts/verifyCommit.js
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const msg = require('fs')
.readFileSync('.git/COMMIT_EDITMSG', 'utf-8')
.trim()
const commitRE = /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?: .{1,50}/
const mergeRe = /^(Merge pull request|Merge branch)/
if (!commitRE.test(msg)) {
if(!mergeRe.test(msg)){
console.log('git commit信息校验不通过')
console.error(`git commit的信息格式不对, 需要使用 title(scope): desc的格式
比如 fix: xxbug
feat(test): add new
具体校验逻辑看 scripts/verifyCommit.js
`)
process.exit(1)
}
}else{
console.log('git commit信息校验通过')
}
执行流程
- git init 初始化git项目
- git commit -m 'xxx'
- 触发.husky 钩子 .husky\commit-msg
- 执行对应的js文件
2. 权限
vite模拟mock
npm i mockjs -D
npm i vite-plugin-mock -D
src/mock/index.ts
import { MockMethod } from 'vite-plugin-mock'
export default [
{
url: '/api/getUserInfo', // 注意,这里只能是string格式
method: 'get',
response: () => {
return 'hello world and get mockData'
}
}
] as MockMethod[]
vue实现
- login页面判断是否有权限
- 登录成功后,token存localsotrage本地
- 修改axios头信息,加入token,请求接口
- 通过返回的状态码,配合路由的router.beforeEach全局拦截
登录
handleLogin() {
formRef.value.validate(async valid => {
if (valid) {
loading.value = true
const {code, message} = await useStore.login(loginForm)
loading.value = false
if(code===0){
router.replace( toPath || '/')
}else{
message({
message: '登录失败',
type: 'error'
})
}
} else {
console.log('error submit!!')
return false
}
})
}
路由配置
import Login from '../components/Login.vue'
const routes = [
...
{
path: '/login',
component: Login,
hidden: true,
}
]
发起网络请求
{
url: '/geek-admin/user/login',
type: 'post',
response: config => {
const { username } = config.body
const token = tokens[username]
// mock error
if (user!=='dasheng') {
return {
code: 60204,
message: 'Account and password are incorrect.'
}
}
return {
code: 20000,
data: token
}
}
}
请求拦截
service.interceptors.request.use(
config => {
const token = getToken()
// do something before request is sent
if (token) {
config.headers.gtoken = token
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
路由守卫
router.beforeEach(async (to, from,next) => {
// canUserAccess() 返回 `true` 或 `false`
let token = getToken()
if(!token){
next('/login')
}
return true
})
传统cookie方式
- 原理:直接使用服务器
res.cookie('name', 'value', { maxAge: 900000, httpOnly: true });写入cookie到浏览器,每次浏览器会带上cookie发起请求,也叫做session - 缺点 - 当前后端分离时候,并且部署的不同的机器上,session不能做到唯一
- 推荐使用token解决
- token过期
- 后台校验,返回特殊码,前端根据特殊码提示过期 - 存储使用vuex+ localstorage
角色权限
- RBAC(Role-Based Access Control,基于角色的访问控制) 用户 -> 角色 -> 页面
- 登录的时候根据权限,返回有权限的路由
- 把当前路由与 后台动态路由合并
addRoutes({ commit }, accessRoutes) {
// 添加动态路由,同时保存移除函数,将来如果需要重置路由可以用到它们
const removeRoutes = []
accessRoutes.forEach(route => {
const removeRoute = router.addRoute(route)
removeRoutes.push(removeRoute)
})
commit('SET_REMOVE_ROUTES', removeRoutes)
},
3. 集成第三方库
集成NProgress
import NProgress from 'nprogress' // progress bar
router.beforeEach(async (to, from, next) => {
// start progress bar
NProgress.start()
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
注意事项
避免使用mixins,extends
- 隐式添加的api无法溯源
- 命名冲突
vue3全局使用
app.config.globalProperties.x 注册访问
自定义组件chart
通过 onmount 和unmonut 控制加载与卸载
<template>
<div ref="chartRef" class="chart"></div>
</template>
<script setup>
import * as echarts from 'echarts'
import {ref,onMounted,onUnmounted} from 'vue'
// 通过ref获得DOM
let chartRef = ref()
let myChart
onUnmounted(()=>{
myChart.dispose()
myChart = null
})
onMounted(()=>{
myChart = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'item'
},
color: ['#ffd666', '#ffa39e', '#409EFF'],
// 饼图数据配置
series: [
{
name: 'title',
type: 'pie',
radius: '60%',
data: [
{value: 43340, name: '1111'},
{value: 7003, name: '2222'},
{value: 4314, name: '3333'}
]
}
]
}
myChart.setOption(option)
})
</script>
自定义指令
const lazyPlugin = {
install (app, options) {
app.directive('lazy', {
mounted: ...,
updated: ...,
unmounted: ...
})
}
}
4. 性能优化
从输入url 到显示页面浏览器都做了啥
1. dns解析
可以做预解析
建立连接,三次握手
- 优化减少请求的数量
- 雪碧图
- http2
- 优化请求的大小
- 图片格式调整使用 webp png
- 压缩图片
- 开启gzip
- 懒加载
- 到可视区域才开始加载
- 路由懒加载
- vite可视化插件
npm i rollup-plugin-visualizer
npm run build
查看dist报告
下载 html js css
代码执行效率
- 算法优化
- 递归改递推遍历的方式
- 尽量使用temlate语法,让vue3自动做优化
用户体验优化
- 失去一点点性能,提高体验
- 模糊的占位符到 原图
- 大文件不一次性上传,分多几次上传,再合并
- 先渲染鱼骨图,再显示内容
性能检测报告
- FCP
- First Contentful Paint 首次显示第一个dom的时间
- TTI
- Time to interactive 可以交互的时间
- 使用performance 对象
- 自己通过start和end计算时间差
5. 自动化部署
git actions + docker
流程
- 代码提交
- 服务器检测变化
- 自动打包html到项目服务器
- 自动打包cdn的到cdn服务器
代码
name: 打包应用的actions
on:
push: # 监听代码时间
branches:
- master # master分支代码推送的时候激活当前action
jobs:
build:
# runs-on 操作系统
runs-on: ubuntu-latest
steps:
- name: 迁出代码
uses: actions/checkout@master
# 安装Node
- name: 安装Node
uses: actions/setup-node@v1
with:
node-version: 14.7.6
# 安装依赖
- name: 安装依赖
run: npm install
# 打包
- name: 打包
run: npm run build
预发布
- 都是正式环境数据
- 只有开发和测试使用
支持版本回滚
- 根据版本号回滚
AB测试
- 上线的时候给某个区域的用户先试用
- 稳定后再推广全国
部署结束后通知
- 通过集成钉钉,或邮件通知关键用户
- 内容:版本号、部署日期、发起人
6.什么是好的项目
有亮点的项目
对现有普通项目做更进一步的优化
组长建议
1. 提高研发效率
- 团队的脚手架
- 内部基础组件库 :测试覆盖率90%,稳定
2. 稳定性
- CI/CD项目 使用git actions
- 研发低代码平台
3. 性能
- 性能监控系统
- 提前通过日志预测用户行为
- 实时监控客户端是否有报错
使用STAR法则来描述自己的项目
7.TS
TS定义
ts是js外再包裹一层,是js的超集
- 好处
- 更好的类型提示,开发阶段就可以报错提示
- 阅读源码更容易: vue3中都定义好类型,vue2全是this 难以理解
常用关键字
interface
限制对象定义
interface Obj {
name:string,
age:number,
score:number
}
let obj:Obj = {
name : '张三',
age:18,
score : ''//报错必须是数字
}
enum 枚举
enum ColorType {
RED = 1,
GREEN = 2,
YELLOW = 3
}
console.log(ColorType.RED == 1)
enum ColorType2 {
RED,//不赋值默认从0开始
GREEN,//1
YELLOW//2
}
enum ColorType3 {
RED = 'red',//
GREEN = 'green',//
YELLOW = 'yellow'//
}
联合 |
基本类型联合
let b:boolean|string = false
b = 'dd'
b = true
自定义类型联合
type mytype = 100 | 200 | 300
let f:mytype = 90 // 失败,只能是 100,200,300
函数
type 定义函数
type myfunType2 = ( para1:string, para2:number) => string
const myfun2:myfunType2 = (para1:string,para2:number):string =>{
return ''
}
interface定义函数
interface myfunType{
( para1:string,
para2:number):string
}
const myfun1:myfunType = (para1:string,para2:number):string =>{
return para1 + para2
}
返回参数使用 :xxxx定义
泛型
基本用法
//传入的类型是string , 则参数要string,返回也必须是string
function mytest<T>(args:T):T {
return args
}
const bbb:string = mytest<string>('111')
使用
function myfun<某种类型>(args:某种类型):某种类型{
return args
}
//使用的使用必须要 泛型一致
myfun<String>('xxxx')//正确
myfun<String>(100)//错误
属性校验
interface CourseType {
name:string,
price:number[],
img?:string|boolean,
}
let myCourse: CourseType = {
name:'title1111',
price:[100,233,500],
img:'xxx', //这里只能使用字符串或者布尔值
}
// T 类型
// K 属性
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]
}
getProperty(myCourse,'name') // ok
getProperty(myCourse,'name1') // 提示没有定义这个属性
vue3的支持
常用ref reactive computed 支持<xx>调用
const a = ref<Number>(99)//传入必须是数字
const b = reactive<String>('xxx')//传入必须是字符串
const c = computed<String>( () => {
return 'xxx' //返回必须是字符串
})
- Props和emits
const props = defineProps<{
title: string
value?: number
}>()
const emit = defineEmits<{
(e: 'update', value: number): void
}>()
高阶
keyof
interface Obj2 {
name:string,
age:number,
score?:number
}
type mykeys = keyof Obj2
//等价于
type mykeys2 = 'name' | 'age' | 'score'
let a1:mykeys = 'name1'//提示错误,必须为name
extends 条件判断
type ExtendsType<T> = T extends number ? 'age': 'name'
type typeAge = ExtendsType<number>
type typeName = ExtendsType<string>
let a3:typeName = 'name' //必须是name
let a4:typeAge = 'age' //必须是age
in 实现循环
type mykeys = 'name' | 'age' | 'score'
type MyObj = {
[key in mykeys]:string
}
//等价于
type MyObj2 = {
name:string,
age:string,
score:string
}
infer更细的条件判断
必须要extends 配合使用
type Foo = () => string
// 如果T是一个函数,并且函数返回类型是P就返回P
type ReturnType1<T> = T extends ()=>infer P ?P:never
type Foo1 = ReturnType1<Foo>
练习题
- 实现类型函数
interface Todo {
title: string
desc:string
done: boolean
}
type partTodo = Partial1<Todo>
type Partial1<T> = {
[K in keyof T]?:T[K]
}
let aa:Partial1 = {
title: '1',
desc:'1',
done: false,
}
- 后端接口约定
import axios from 'axios'
interface Api{
'/course/buy':{
id:number
},
'/course/comment':{
id:number,
message:string
}
}
function request<T extends keyof Api>(url:T,obj:Api[T]){
return axios.post(url,obj)
}
request('/course/buy',{id:1})
request('/course/comment',{id:1,message:'xxxx'})
request('/course/comment',{id:1}) //如果message必传
request('/course/404',{id:1}) //接口不存在 类型怎么需要报错