vue3项目实战
-知乎日报第3天import { Button } from 'vant'
import { ref, useAttrs, useSlots } from 'vue'
// 把传递的属性,去除特殊的,其余的都赋值给Vant内部的组件
const filter = (attrs) => {
let props = {}
Reflect.ownKeys(attrs).forEach((key) => {
if (key === 'loading' || key === 'onClick') return
props[key] = attrs[key]
})
return props
}
const ButtonAgain = {
inheritAttrs: false,
setup() {
const attrs = useAttrs(),
slots = useSlots()
// 自己控制loading效果
const loading = ref(false)
const handle = async (ev) => {
loading.value = true
try {
await attrs.onClick(ev)
} catch (_) {}
loading.value = false
}
console.log(`1- 非计算属性版`)
return () => {
console.log(`2- 非计算属性版`)
let props = filter(useAttrs())
return (
)
}
}
}
export default ButtonAgain
import { Button } from 'vant'
import { ref, useAttrs, useSlots, computed } from 'vue'
const ButtonAgain = {
inheritAttrs: false,
setup() {
const attrs = useAttrs(),
slots = useSlots()
const props = computed(() => {
let attrs = useAttrs()
let props = {}
Reflect.ownKeys(attrs).forEach((key) => {
if (key === 'loading' || key === 'onClick') return
props[key] = attrs[key]
})
return props
})
// 自己控制loading效果
const loading = ref(false)
const handle = async (ev) => {
loading.value = true
try {
await attrs.onClick(ev)
} catch (_) {}
loading.value = false
}
console.log(`计算属性版`)
return () => {
return (
)
}
}
}
export default ButtonAgain
努力加载中,请稍后
import { createVNode, render } from 'vue'
import Index from './Index.vue'
export default function showOverlayLoading() {
// 创建虚拟DOM。
let vnode = createVNode(Index)
// 渲染虚拟DOM
// console.log(`vnode-->`, vnode);
const frag = document.createDocumentFragment()
render(vnode, frag)
document.body.appendChild(vnode.el, frag)
return function hiddenOverlayLoading() {
if (vnode?.el) {
document.body.removeChild(vnode.el)
vnode = null
}
// render(null, frag)
}
}
{{ state.btn.text }}
立即登录/注册
src/stores/base.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import API from '@/api'
const useBaseStore = defineStore('base', () => {
// 定义公共状态。
const profile = ref(null)
// 修改公共状态。
//
const queryProfile = async () => {
let info = null
try {
let { code, data } = await API.userInfo()
if (code === 0) {
info = data
profile.value = info
}
} catch (error) {
console.log(`error:-->`, error)
}
return info
}
// 暴露给外面用。
return {
profile,
queryProfile
}
})
export default useBaseStore
src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import useBaseStore from '@/stores/base'
import { showFailToast } from 'vant'
const router = createRouter({
history: createWebHashHistory(),
routes
})
// 全局前置守卫:登录态校验
const checkList = ['/person', '/store', '/update']//用于判断那些页面需要登录态校验。
router.beforeEach(async (to, from, next) => {
const base = useBaseStore()//用于拿到个人信息。
let profile = base.profile
if (checkList.includes(to.path) && !profile?.value) {
let info = await base.queryProfile()
if (!info) {
// 真的没登录过。
showFailToast('您还未登录,请先登录')
next({
path: '/login',
query: {
target: to.fullPath
}
})
return
}
}
next()
})
// 全局后置守卫
router.beforeEach((to, from) => {
})
export default router
src/views/Login.vue
立即登录/注册
会有一个问题-路由错乱的问题。
立即登录/注册
先写一个主组件。主组件可以用模板组件,也可以用jsx组件
src/components/overlay/Index.vue 模板组件
努力加载中,请稍后
src/App.vue 在根视图中先看模板组件效果
写一个js函数,用于在全局中渲染组件和移除主组件。
src/components/overlay/Index.vue 主组件
努力加载中,请稍后
src/components/overlay/index.js 在js文件中用js方式来在全局中插件入调用。
import { createVNode, render } from 'vue'
import Index from './Index.vue'
export default function showOverlayLoading() {
// 创建虚拟DOM。
const vnode = createVNode(Index)
// 渲染虚拟DOM
console.log(`vnode-->`, vnode);
const frag = document.createDocumentFragment()
render(vnode, frag)
document.body.appendChild(vnode.el, frag)
return function hiddenOverlayLoading() {
render(null, frag)
}
}
src/App.vue 根组件中尝试调用
全局loading模板组件
努力加载中,请稍后
全局loading模板组件
的方法import { createVNode, render } from 'vue'
import Index from './Index.vue'
export default function showOverlayLoading() {
// 创建虚拟DOM。
const vnode = createVNode(Index)
// 渲染虚拟DOM
console.log(`vnode-->`, vnode);
const frag = document.createDocumentFragment()
render(vnode, frag)
document.body.appendChild(vnode.el, frag)
return function hiddenOverlayLoading() {
render(null, frag)
}
}
import showOverlayLoading from '@/components/overlay'
// 全局前置守卫:登录态校验
const checkList = ['/person', '/store', '/update']//用于判断那些页面需要登录态校验。
let hiddenOverlayLoading = null//用于遮罩层
router.beforeEach(async (to, from, next) => {
if (需要进行登录但没个人信息时) {
hiddenOverlayLoading = showOverlayLoading()//开启遮罩层
let info = await base.queryProfile()//异步用token拿到个人信息。
if (!info) {
// 真的没登录过。
showFailToast('您还未登录,请先登录')
next({
path: '/login',
query: {
target: to.fullPath
}
})
hiddenOverlayLoading?.()//移除遮罩层-用户真的没登录时。
return
}
}
next()
})
// 全局后置守卫
router.beforeEach((to, from) => {
hiddenOverlayLoading?.()//移除遮罩层-其它情况,如用户已登录或者是无需个人信息页的情况。
})
export default router
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import useBaseStore from '@/stores/base'
import { showFailToast } from 'vant'
import showOverlayLoading from '@/components/overlay'
const router = createRouter({
history: createWebHashHistory(),
routes
})
// 全局前置守卫:登录态校验
const checkList = ['/person', '/store', '/update']//用于判断那些页面需要登录态校验。
let hiddenOverlayLoading = null//用于遮罩层
router.beforeEach(async (to, from, next) => {
const base = useBaseStore()//用于拿到个人信息。
let profile = base.profile
if (checkList.includes(to.path) && !profile) {
hiddenOverlayLoading = showOverlayLoading()//开启遮罩层
let info = await base.queryProfile()
if (!info) {
// 真的没登录过。
showFailToast('您还未登录,请先登录')
next({
path: '/login',
query: {
target: to.fullPath
}
})
hiddenOverlayLoading?.()//移除遮罩层
return
}
}
next()
})
// 全局后置守卫
router.beforeEach((to, from) => {
hiddenOverlayLoading?.()//移除遮罩层
let title = to.meta?.title
document.title = !title ? '知乎日报' : `${title} - 知乎日报`
})
export default router
src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
const router = createRouter({
history: createWebHashHistory(),
routes
})
// 全局后置守卫
router.beforeEach((to, from) => {
let title = to.meta?.title
document.title = !title ? '知乎日报' : `${title} - 知乎日报`
})
export default router
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import useBaseStore from '@/stores/base'
import { showFailToast } from 'vant'
import showOverlayLoading from '@/components/overlay'
const router = createRouter({
history: createWebHashHistory(),
routes
})
// 全局前置守卫:登录态校验
const checkList = ['/person', '/store', '/update']//用于判断那些页面需要登录态校验。
let hiddenOverlayLoading = null//用于遮罩层
router.beforeEach(async (to, from, next) => {
const base = useBaseStore()//用于拿到个人信息。
let profile = base.profile
if (checkList.includes(to.path) && !profile?.value) {
hiddenOverlayLoading = showOverlayLoading()//开启遮罩层
let info = await base.queryProfile()
if (!info) {
// 真的没登录过。
showFailToast('您还未登录,请先登录')
next({
path: '/login',
query: {
target: to.fullPath
}
})
hiddenOverlayLoading?.()//移除遮罩层
return
}
}
next()
})
// 全局后置守卫
router.beforeEach((to, from) => {
hiddenOverlayLoading?.()//移除遮罩层
let title = to.meta?.title
document.title = !title ? '知乎日报' : `${title} - 知乎日报`
})
export default router
src/router/routes.js
import Home from '@/views/Home.vue'
const routes = [{
path: '/',
name: 'home',
meta: { title: '首页' },
component: Home
}, {
path: '/detail/:id',
name: 'detail',
meta: { title: '详情页' },
component: () => import('@/views/Detail.vue')
}, {
path: '/login',
name: 'login',
meta: { title: '登录/注册页' },
component: () => import('@/views/Login.vue')
}, {
path: '/person',
name: 'person',
meta: { title: '个人中心' },
component: () => import('@/views/Person.vue')
}, {
path: '/store',
name: 'store',
meta: { title: '我的收藏' },
component: () => import('@/views/Store.vue')
}, {
path: '/update',
name: 'update',
meta: { title: '更改信息' },
component: () => import('@/views/Update.vue')
}, {
path: '/:pathMatch(.*)*',
redirect: '/'
}]
export default routes
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import useBaseStore from '@/stores/base'
import { showFailToast } from 'vant'
import showOverlayLoading from '@/components/overlay'
const router = createRouter({
history: createWebHashHistory(),
routes
})
// 全局前置守卫:登录态校验
const checkList = ['/person', '/store', '/update']//用于判断那些页面需要登录态校验。
let hiddenOverlayLoading = null//用于遮罩层
router.beforeEach(async (to, from, next) => {
const base = useBaseStore()//用于拿到个人信息。
let profile = base.profile//个人信息。
// 除登录页之外,其余所有页面在没有存储登录者信息的情况下,都需要从服务器获取登录者信息进行存储。
if (!profile && to.path !== '/login') {
hiddenOverlayLoading = showOverlayLoading()//开启遮罩层
let info = await base.queryProfile()
// 如果是需要登录态校验的三个页面,再进行登录校验和跳转。
if (checkList.includes(to.path) && !info) {
// 真的没登录过。
showFailToast('您还未登录,请先登录')
next({
path: '/login',
query: {
target: to.fullPath
}
})
hiddenOverlayLoading?.()//移除遮罩层
return
}
}
next()
})
// 全局后置守卫
router.beforeEach((to, from) => {
hiddenOverlayLoading?.()//移除遮罩层
let title = to.meta?.title
document.title = !title ? '知乎日报' : `${title} - 知乎日报`
})
export default router
import { defineStore } from 'pinia'
import { ref } from 'vue'
import API from '@/api'
const useCollectStore = defineStore('collect', () => {
// 定义公共状态。
// 派发的方法。
// 暴露给外面用。
return {
}
})
export default useCollectStore
import { defineStore } from 'pinia'
import { ref } from 'vue'
import API from '@/api'
const useCollectStore = defineStore('collect', () => {
// 定义公共状态。
const collectList = ref(null)
// 派发的方法。
const queryCollectList = async () => {
}
const removeCollectList = async () => {
}
// 暴露给外面用。
return {
collectList,
queryCollectList,
removeCollectList,
}
})
export default useCollectStore
示例代码:
src/stores/collect.js 收藏相关的接口都用来源于这里的文件。
import { defineStore } from 'pinia'
import { ref } from 'vue'
import API from '@/api'
import { showFailToast, showSuccessToast } from 'vant'
const useCollectStore = defineStore('collect', () => {
// 定义公共状态。
const collectList = ref(null)//用于保存收藏列表。
// 派发的方法。
// 查询收藏列表。
const queryCollectList = async () => {
let list = null
try {
let { code, data } = await API.storeList()
if (+code === 0) {
list = data
collectList.value = list
}
} catch (error) {
console.log(`error:-->`, error)
}
return list
}
// 删除收藏。
// id为收藏id。
const removeCollectList = async (id) => {
if (!collectList?.value) {
return
}
try {
let { code } = await API.storeRemove(id)
if (+code !== 0) {
showFailToast('移除收藏失败')
return
}
showSuccessToast(`移除收藏成功`)
collectList.value = collectList.value.filter(item => {
return +item.id !== +id
})
} catch (error) {
console.log(`error:-->`, error)
}
}
// 新增收藏。
const insertCollectList = async (newsId) => {
try {
let { code } = await API.storeAdd(newsId)
if (+code !== 0) {
showFailToast('收藏失败')
return
}
await queryCollectList()
showSuccessToast(`收藏成功`)
} catch (error) {
console.log(`error:-->`, error)
}
}
// 暴露给外面用。
return {
collectList,
queryCollectList,
removeCollectList,
insertCollectList,
}
})
export default useCollectStore
src/views/Detail.vue 详情页
replace()
进登录页,用target字段
标识,则在登录成功后
,退回到详情页。
target字段
标识,则在登录成功后
,跳转到target字段
对应的路径中。replace()
,也会丢失历史记录。在登录页中点击我们写的后退组件
,不是返回详情页
,而是回到详情页的上一条历史记录
。即在详情页用replace()
进登录页之后,从登录页
点击后退,会跳转回首页
。
我们写的后退组件
中做特殊处理。
src/components/NavBack.vue
src/views/Login.vue
{{ state.btn.text }}
立即登录/注册
关于没登录跳转到登录页的核心处理代码:
src/views/Detail.vue 详情页
replace()
进登录页,用target字段
标识,则在登录成功后
,退回到详情页。
target字段
标识,则在登录成功后
,跳转到target字段
对应的路径中。replace()
,也会丢失历史记录。在登录页中点击我们写的后退组件
,不是返回详情页
,而是回到详情页的上一条历史记录
。即在详情页用replace()
进登录页之后,从登录页
点击后退,会跳转回首页
。
我们写的后退组件
中做特殊处理。
src/components/NavBack.vue
src/views/Login.vue
立即登录/注册
vite按需导入插件vite-plugin-imp
与vant@4的按需导入
插件有冲突,会导致vant4中的函数调用式组件
会导入与实际vant组件用到的样式文件地址
不同的路径。示例
vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import viteImp from 'vite-plugin-imp'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'
import pxtorem from 'postcss-pxtorem'
/* https://vitejs.dev/config/ */
export default defineConfig({
base: './',
plugins: [
vue(),
vueJsx(),
/* // 按需导入插件 https://github.com/onebay/vite-plugin-imp
// 与vant4的按需导入有冲突。
viteImp({
libList: [
{
libName: 'lodash',
libDirectory: '',
camel2DashComponentName: false
}
]
}), */
// vant@4的按需导入
Components({
resolvers: [
VantResolver()
]
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
/* 服务配置 */
server: {
host: '127.0.0.1',
proxy: {
'/api': {
target: 'http://127.0.0.1:7100',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
},
/* 生产环境 */
build: {
assetsInlineLimit: 1024 * 10,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
rollupOptions: {
external: ['']
}
},
/* CSS样式 */
css: {
postcss: {
plugins: [
pxtorem({
rootValue: 37.5,
propList: ['*']
})
]
},
preprocessorOptions: {
less: {
additionalData: `@import "@/assets/var.less";`
}
}
}
})
核心:
vite.config.js
import viteImp from 'vite-plugin-imp'
export default defineConfig({
plugins: [
// 按需导入插件 https://github.com/onebay/vite-plugin-imp
// 与vant4的按需导入有冲突。
viteImp({
libList: [
{
libName: 'lodash',
libDirectory: '',
camel2DashComponentName: false
}
]
}),
],
})
不兼容
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
Components({
resolvers: [
VantResolver()
]
})
],
})
ts
代替js
,用tsx
代替jsx
。/*
let/const 变量:类型限定 = 值
+ 变量不能是已经被 lib.dom.d.ts 声明的,例如:name
但可以把当前文件变为一个模块 “ 加入 export 导出 ”,这样在这里声明的变量都是私有的了
+ 类型限定可以是小写和大写
+ 一般用小写
+ 大写类型可以描述实例
+ 大写的 Object 不用,因为所有值都是其实例;想要笼统表示对象类型,需要用 object !
+ 数组的限定
let arr:number/string[]
let arr:(number|string)[]
let arr:Array 泛型
...
+ TS中的元祖:类型和长度固定
let tuple:[string, number] = ['abc', 10]
可基于数组的方法操作元祖
+ TS中的枚举
enum USER_ROLE {
ADMIN,
USER
}
+ null 和 undefined 只能赋予本身的值
+ void 用于函数的返回
function fn():void{ ... }
function fn():void | null{ ... }
+ never 不可能出现的值「任何类型的子类型」
function fn():never{
// 报错 OR 死循环 等
}
+ any 任意类型
*/
人格保证
了,就是不是,ts编译器
也会把该值当成是断言的类型。/*
类型断言:
@1 声明变量,没有给类型限定,没有赋值的时候,默认类型是any
@2 如果最开始声明的时候赋值了,则会按照此时值的类型自动推导
@3 联合类型
let name:string | number
+ 在没有赋值之前,只能使用联合类型规定的类型,进行相关的操作
+ 不能在变量赋值之前调用其方法
+ !. 断言变量一定有值
+ as 认定是啥类型的值
(name! as number).toFixed()
@4 字面量类型
let direction:'top'|'right'|'down'|'left' 赋的值只能是这四个中的一个{限定了值}
可以基于 type (类型别名)优化
let Direction = 'top'|'right'|'down'|'left'
let direction:Direction = ...
*/
/*
函数的玩法
普通函数:声明参数和返回值类型
function fn(x:number,y:number):number{...}
函数表达式:在普通函数基础上,对赋值的函数做类型校验
type Fn = (x:number,y?:number) => number
let fn:Fn = function(x,y){...}
*/
@vue/cli
中使用vite
中使用Vue3进阶/knowledge/env.d.ts 这个很重要,要不在.vue后缀类型文件
中会有报错。
///
// 声明导入 .vue 文件的类型「防止波浪线报错」
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
Vue3进阶/knowledge/tsconfig.app.json
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue"
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
}
}
Vue3进阶/knowledge/tsconfig.json 看对应的pdf文档
。
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}
Vue3进阶/knowledge/tsconfig.node.json
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"module": "ESNext",
"types": [
"node"
]
}
}
BOSS直聘
一天100个左右,其它投到上限。(用一个小时左右投)BOSS直聘
(主要)
投的时间:周一到周六,每天9:30开始、下午14:00开始(不要睡懒觉)。
老家或北京之类的都投。
职业规划
离职原因