前端系列第10集-实战篇

news2024/11/18 15:51:37

用户体验:性能,交互方式,骨架屏,反馈,需求分析等

组件库:通用表单,表格,弹窗,组件库设计,表单等

项目质量:单元测试,规范,监控,报警,monorepo等

性能优化:性能指标,代码更快,文件加载更快,框架优化,优化方案分析等

普通项目:登录注册,布局,增删改查等

研发效率:脚手架,组件库,开发规范,联调效率,自动化等

vue3+ts:vue3,vite,pinia,组件库,vue-router等

工具库:axios,工程化,工具库,pnpm,typescript等

大数据量

如果直接渲染1W行列表,不出意外你的页面就要卡了,比较常见的优化方案就是虚拟滚动,就是只渲染你能看到的视窗中的几十行,然后通过监听滚动来更新这几十个dom

  • 可视区的高度固定 viewHeight (clientHeight

  • 每个列表高度height (固定

  • 可视区域的数据索引start和end (scrollTop / height

  • 基于startIndex计算出offset偏移(scrollTop - (scrollTop % height);

  • 渲染数据 & 监听滚动事件

// 列表容器的dom
  const container = useRef<HTMLDivElement>(null)
  // 开始位置
  const [start, setStart] = useState(0)
  // 视图中的数据
  const [visibleData, setVisibleData] = useState<VirtualProps['list']>([])
  // 控制偏移量
  const [viewTransfrom, setViewTransfrom] = useState('translate3d(0,0,0)')
  useEffect(() => {
    const containerDom = container.current
    const viewHeight = containerDom?.clientHeight || 500 // 视窗高度
    const visibleCount = Math.ceil(viewHeight / HEIGHT) // 视窗内有几个元素
    const end = start + visibleCount
    setVisibleData(list.slice(start, end))
  }, [])
  function handleScroll(e: React.UIEvent<HTMLDivElement, UIEvent>) {
    const scrollTop = e.currentTarget.scrollTop // 滚动的距离
    const containerDom = container.current
    const viewHeight = containerDom?.clientHeight || 500 // 视窗高度

    const start = Math.floor(scrollTop / HEIGHT)
    const end = start + Math.ceil(viewHeight / HEIGHT)
    setVisibleData(list.slice(start, end))
    setStart(start)
    setViewTransfrom(`translate3d(0,${start * HEIGHT}px,0)`)
  }
// 预估高度60
const PREDICT_HEIGHT = 60
  // 不定高数组,维护一个位置数据
const [positions, setPosition] = useState<{ top: number;height: number }[]>([])

// 渲染数组之后,更新positions数组
Array.from(listDom?.children).forEach((node, index) => {
  const { height } = node.getBoundingClientRect()
  // console.log(start+index, node.id)
  if (height !== positions[start + index].height) {
    setPosition((prev) => {
      const newPos = [...prev]
      newPos[start + index].height = height
      for (let k = index + 1; k < prev.length; k++)
        newPos[k].top = newPos[k - 1].top + newPos[k - 1].height
      return newPos
    })
  }
})
}, [visibleData])

文件上传

  1. 文件切片 + 秒传 + 暂停

  2. 文件计算hash值,就像文件的身份证号,用来问后端有没有切片存在

  3. 计算hash的卡顿 可以使用web-worker,时间切片,抽样Hash三种解决方案

  4. 上传文件切片

async handleVerify(req, res) {
 const data = await resolvePost(req)
 const { filename, hash } = data
 const ext = extractExt(filename)
 const filePath = path.resolve(this.UPLOAD_DIR, `${hash}${ext}`)
 
 //文件是否存在
 let uploaded = false
 let uploadedList = []
 if (fse.existsSync(filePath)) {
  uploaded = true
 } else {
  // 文件没有完全上传完毕,但是可能存在部分切片上传完毕了
  uploadedList = await getUploadedList(path.resolve(this.UPLOAD_DIR, hash))
 }
 res.end(
  JSON.stringify(
   uploaded,
   uploadedList // 过滤诡异的隐藏文件
  })
 )
}

web-worker计算md5

async calculateHash(chunks) {
 return new Promise(resolve => {
  // web-worker 防止卡顿主线程
  this.container.workder = new Worker("/hash.js");
  this.container.workder.postMessage({ chunks });
  // 等通知
  this.container.workder.onmessage = e => {
   const { progress, hash } = e.data
   this.hashProgress = Number(progress.toFixed(2));
   if (hash) {
    resolve(hash);
   }
  };
 });
}

const workLoop = async deadline => {
 // 有任务,并且当前帧还没有结束
 while (count < chunks.length && deadline.timeRemaining() > 1) {
  await appendToSpark(chunks[count].file);
  count++;
  // 没有了 计算完毕
  if (count < chunks.length) {
   // 计算中
   this.hashProgress = Number(
    ((100 * count) / chunks.length).toFixed(2)
   );
  } esle {
   this.hashProgress = 100;
   // 计算任务结束
   resolve(spark.end());
  }
 }
 // 当前帧没有时间了,说明浏览又渲染任务了
 window.requestIdleCallback(workLoop);
 };
 window.requestIdleCallback(workLoop);
});
function limit(maxCount) {
 // 任务队列
 let queue = []
 let activeCount = 0
 
 const next = () => {
  // 下一个任务
  activeCount--
  if(queue.length>0) {
   queue.shift()()
  }
 }
 const run = async (fn,resolve,args) => {
  // 执行一个函数
  activeCount++
  const result = (async() => fn(...args))()
  resolve(result)
  await result
  next()
 }
 const push = async (fn,resolve,args) => {
  queue.push(run.bind(null, fn, resolve, args))
  if (activeCount < maxCount && queue.length > 0) {
   // 队列没满,并且还有任务,启动任务
   queue.shift()()
  }
 }
 let runner = (fn, ...args) => {
  return new Promise((resolve) => {
   push(fn, resolve, args)
  })
 }
 return runner
}

async function asyncPool({
 limit,
 items,
 fn
}) {
 const promises = []
 const pool = new Set()
 for (const item of items) {
  const promise = fn(item)
  promises.push(promise)
  pool.add(promise)
  const clean = () => pool.delete(promise)
  promise.then(clean, clean)
  if (pool.size >= limit) await Promise.race(pool)
 }
 return Promise.all(promises)
}

完整的构建打包流程/服务(统一的脚手架、上线服务等)、完整的测试环境、前端错误日志管理系统(收集、统计、报警)、前端资源离线化管理、前端资源增量下载服务以及针对Node应用的日志(完整调用链)、性能和错误监控平台等等。

runner的执行方式有很多种, 目前最流行的就是作为一个docker容器,其内部集成了gitlab的一些基础环境, 注册阶段就是将其与gitlab主任务做关联(runner通常不跟gitlab服务器部署在同一台服务器),而yaml中配置的任务,就是在runner中具体执行, 然后将结果发送回gitlab服务器。

最后项目需要在setttings中开启enable shared runner或者specific runner.

使用Node搭建服务,托管静态资源,以及代理请求的转发。

runner中执行yaml中的task

  • 资源构建
    针对测试环境打包: npm run build -e test

  • 上传资源到node 服务器。
    将该服务抽离为npm 包, 执行festaging-scripts命令,上传的资源有两类:

    • 构建出的静态资源

    • 必要的请求代理配置

function buildUrl(prefix) {}

var originXHROPEN = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
 return originXHROpen.call(this, method, buildUrl(url, '${prefix}'), async, user, password);
 
if (window.fetch) {
 var originFetch = window.fetch;
 window.fetch = function () {
  var input = arguments[0];
  if (typeof input === 'string') {
   arguments[0] = buildUrl(input, '${prefix}');
  }
  return originFetch.apply(this, arguments);
 };
}

统一项目新建入口、项目开发模板,项目开发流程。节省新成员上手成本。

团队成员可以通过输入项目名、GitLab 组、项目模板等字段直接创建 GitLab 仓库,并根据选择的模板及名称等信息在已创建的 GitLab 仓库里进行项目初始化。

  • 量化团队代码质量,统计团队工作量,监测业务吞吐量变化等。

  • 在团队中推行 Commit 提交规范。

  • 获取团队成员的 Git Commit 信息,并存入数据库,以 Commit 信息数据为基础做数据统计分析。

  • fix、feat、style、refactor 等开头的 Commit 可绘制饼图进行统计对比

需要注意的有

  • 有些操作或导致 Commit 重复,所以对于同一个人的 Commit 需要做去重。

  • GitLab Events 只会存近一年的数据。所以运行当天也只能统计近一年的代码。

  • 一定要按人进行 Push 时间划分,这样第一次运行之后,后面就可以只取上次取的最后一次 Push 的时间之后的 Commit 了。请求数可以减少很多。

  • 执行过代码之后发现了一些问题,比如:团队成员误操作将 node_modules 文件夹上传等。这造成了统计代码行数过多,解决办法是过滤掉大于 10000 行(这个可以自由指定)的 commit 。

持续集成基本概念

在传统开发过程中,代码的集成工作通常是在所有工程师们工作完成后进行的,需要单独构建,这往往会花费大量的时间和精力。持续集成是一种将集成工作放在软件开发阶段的做法,以便更加有规律地构建、测试和集成代码。

持续集成可以在开发人员提交了新代码后,立即进行构建、单元测试,可以根据测试结果确定新代码或配置环境是否正确。

7a948e045e327ef4cdea3d12a506645e.png
WeChate8e849f3d9d0a434ad212cf2ae4c8cdc.png

服务器相关操作

安装docker

具体可参考官网:docs.docker.com/engine/inst…[1]

安装gitlab-runer

docker方式安装

# 拉取镜像
docker pull gitlab/gitlab-runner:latest
# 运行镜像
docker run -d --name gitlab-runner --restart always \
        -v /var/run/docker.sock:/var/run/docker.sock \
        -v /srv/gitlab-runner/config:/etc/gitlab-runner \
        gitlab/gitlab-runner

添加用户组及权限

# 添加 用户组及用户
useradd -m -g gitlab-runner gitlab-runner

# 查看系统用户
sudo vim /etc/passwd

# 将下图蓝框内的数字改为0:0,和root保持一致

注册gitlab-runner

# 使用一次性容器来注册 gitlab-runer, --rm 容器推出时清理用户数据
docker run --rm  -it -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register
cf8e25edbd1a53c99a6fbc6baa52e41f.png
image.png
# 输入域名或者服务器ip地址,就是步骤三的url,如果gitlab和要部署的服务器地址不一致,需要做个地址映射哦,自行百度下
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/): 
# 输入token
Please enter the gitlab-ci token for this runner:
# 输入runner描述,可写可不写
Please enter the gitlab-ci description for this runner:
# 给这个Runner指定tags,可以写多个,英文逗号隔开即可
Please enter the gitlab-ci tags for this runner (comma separated):
# 选择Runner是否接受未指定tags的任务,稍后可修改,默认值为false,可写可不写
Whether to run untagged builds [true/false]:
# 选择是否为当前项目锁定Runner,通常用于被指定为某个项目的Runner,默认值为true,可写可不写
Whether to lock the Runner to current project [true/false]: 
# 选择Runner executor(Runner执行器),这里我们选docker哈
Please enter the executor: docker, shell, virtualbox, kubernetes, docker-ssh, parallels, ssh, docker+machine, docker-ssh+machine: 
docker
# docker版本选最新版
Please enter the default Docker image (e.g. ruby:2.6):
docker:latest

# 好了到这一步,看到输出以下语句,就算注册完了,接下来去CI/CD界面下的Runners选项里,看看有没有成功;
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!

docker restart gitlab-runner

 cd ~/.ssh # 查看是否存在密钥
 ssh-keygen -t rsa # 生成密钥
 cat id_rsa    #查看私钥
 cat id_rsa.pub # 查看公钥
ef301b9b3f45f57c185d9951f544eb52.png
image.png

在项目根目录下新增文件gitlab-ci.yml,将以下代码粘贴过去,然后提交代码到test分支

test:build:
    stage: test
    script:
      - docker build -t fast_api .
      - if [ $(docker ps -aq --filter name=trunkverse_service) ]; then docker rm -f trunkverse_service; fi
      - docker run -d -p 8098:8098 --restart=always --name fast_api fast_api 
      - echo 'docker run 完成'
    only:
      - test # 指定test分支一更新则立即构建
    tags:
      - fastApi # 对应每个runner注册时定义的tag

test:deploy:
  image: alpine:3.7
  stage: deploy
  script:
    - echo "http://mirrors.aliyun.com/alpine/v3.7/main/" > /etc/apk/repositories # 下载镜像
    - apk add --no-cache rsync openssh # 安装rsync openssh
    - mkdir -p ~/.ssh
    - echo "$SSH_KEY_PRIVATE" >> /root/.ssh/id_rsa
    - echo "$SSH_KEY_PUB" >> /root/.ssh/id_rsa.pub
    - chmod 700 /root/.ssh/
    - chmod 600 /root/.ssh/id_rsa.pub
    - chmod 600 /root/.ssh/id_rsa
    - echo -e "Host *\n\t StrictHostKeyChecking no \n\n" > ~/.ssh/config
    - rsync -av --delete   ./  $SERVER_HOST:$SERVER_PATH
  only:
    - test
  tags:
    - fastApi

Git 管理方案

master 为生产分支 develop 为开发分支

develop 分支下存在多个功能分支,以 develop 做为基础切出,并会合并回 develop

版本分支 下存在多个环境的版本历史,以版本控制的严格程度分化为多个子分支

master 孑然一身,只有存在紧急 Bug 时,才会有 hotfix分支切入并合并回 master 与 develop

8bb22615906aa046b1f655f3d4b38fdf.png
image.png
# 阶段
stages:
  - install
  - build
  - deploy
  # 缓存 node_modules 减少打包时间,默认会清除 node_modules 和 dist
cache:
  paths:
    - node_modules/

# 安装依赖
install:
  stage: install
  tags: # runner 标签(注册runner时设置的)
    - webpack-vue-cicd
  only:
    changes:
      - package.json
  script: # 执行脚本
    yarn

# 拉取项目,打包
build:
  stage: build # 阶段名称 对应,stages
  tags: # runner 标签(注册runner时设置的,可在 admin->runner中查看)
    - webpack-vue-cicd
  script: # 脚本(执行的命令行)
    - cd ${CI_PROJECT_DIR} # 拉取项目的根目录
    - npm install # 安装依赖
    - npm run build # 运行构建命令
  only:
    - main #拉取分支
  artifacts: # 把 dist 的内容传递给下一个阶
    paths:
      - dist/

# 部署
deploy:
  stage: deploy # 阶段名称 对应,stages
  tags: # runner 标签(注册runner时设置的)
    - webpack-vue-cicd
  script: # 脚本(执行的命令行)
    - rm -rf /www/wwwroot/webpack_vue_cicd/*
    - cp -rf ${CI_PROJECT_DIR}/dist/* /www/wwwroot/webpack_vue_cicd/ # 把包完成,复制 dist 下的文件到对应的项目位置
stages:
  - test
  - build
  - deploy
  
test:
  stage: test
  tags:
    - shell-g-fe-runner
  script:
    - npm install --no-optional --registry=https://registry.npm.taobao.org/
    - npm run lint
stages:
  - test
  - build
  - deploy
  
test:
  stage: test
  tags:
    - shell-g-fe-runner
  script:
    - npm install --no-optional --registry=https://registry.npm.taobao.org/
    - npm run lint
    
build:
  stage: build
  tags:
    - shell-g-fe-runner
  script:
    - sudo npm rebuild node-sass
    - sudo npm run build
  only:
    - master
    - /^beta\/.*$/
    - /^release\/.*$/

GitLab 的 CI 程序同时包含缓存机制,如果你想把你的编译产物缓存下来

build:
  stage: build
  tags:
    - shell-g-fe-runner
  script:
    - sudo npm rebuild node-sass
    - sudo npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 60 mins
deploy_test:
  stage: deploy
  tags:
    - shell-g-fe-runner
  only:
    - /^beta\/.*$/
  environment:
    name: Test
    url: http://test.vue.com/
  script:
    - cp -R dist/* /data/html/vue-com/test/
    
deploy_uat:
  stage: deploy
  tags:
    - shell-g-fe-runner
  only:
    - /^release\/.*$/
  environment:
    name: Uat
    url: https://uat.vue.com/
  script:
    - cp -R dist/* /data/html/vue-com/uat/

deploy_prod:
  stage: deploy
  tags:
    - shell-g-fe-runner
  only:
    - master
  environment:
    name: Production
    url: https://vue.com/
  script:
    - cp -R dist/* /data/html/vue-com/prod/

安装 rsync

deploy_prod:
  stage: deploy
  tags:
    - shell-g-fe-runner
  only:
    - master
  environment:
    name: Production
    url: https://vue.com/
  script:
    - rsync -ravtz --delete --password-file=/data/auth/rsync.pwd dist/* 192.168.1.1::vue-com-prod/

ref() 标注类型有三种方式:

  1. 通过泛型参数的形式来给 ref()增加类型

import { ref } from 'vue'

const initCode = ref<string | number>('200')
  1. 如果是遇到复杂点的类型,可以自定义 interface 然后泛型参数的形式传入

import { ref } from 'vue'

interface User {
  name: string
  age: string | number
}

const user = ref<User>({
  name:'xxx',
  age: 20
})
  1. 通过使用 Ref 这个类型为 ref 内的值指定一个更复杂的类型

import { ref } from 'vue'
import type { Ref } from 'vue'

const initCode: Ref<string | number> = ref('200')

reactive() 返回一个对象的响应式代理。

reactive()标注类型有两种方式:

  1. 直接给声明的变量添加类型

import { reactive } from 'vue'

interface User {
  name: string
  age: string | number
}

const user:User = reactive({
  name:"xxx",
  age:'20'
})
  1. 通过泛型参数的形式来给 reactive()增加类型

import { reactive } from 'vue'

interface User {
  name: string
  age: string | number
}

const user = reactive<User>({
  name:"xxx",
  age:'20'
})

computed()标注类型有两种方式:

  1. 从其计算函数的返回值上推导出类型

import { ref, computed } from 'vue'

const count = ref<number>(0)

// 推导得到的类型:ComputedRef<string>
const user = computed(() => count.value + 'xxx')
  1. 通过泛型参数显式指定 computed() 类型

const user = computed<string>(() => {
  // 若返回值不是 string 类型则会报错
  return 'xxx'
})

为了在声明 props 选项时获得完整的类型推断支持,我们可以使用 defineProps API,它将自动地在 script setup 中使用

  1. 从它的参数中推导类型:

const props = defineProps({
  name: { type: String, required: true },
  age: Number
})
  1. 通过泛型参数来定义 props 的类型

const props = defineProps<{
  name: string
  age?: number
}>()

定义成一个单独的 interface

interface Props {
  name: string
  age?: number
}

const props = defineProps<Props>()
// vite.config.js
export default {
  plugins: [
    vue({
      reactivityTransform: true
    })
  ]
}

通过对 defineProps() 的响应性解构来添加默认值:

<script setup lang="ts">
interface Props {
  name: string
  age?: number
}

const { name = 'xxx', age = 100 } = defineProps<Props>()
</script>

为了在声明 emits 选项时获得完整的类型推断支持,我们可以使用 defineEmits API,它将自动地在 script setup 中使用

defineEmits() 标注类型直接推荐泛型形式

import type { GlobalTheme } from 'naive-ui'

const emit = defineEmits<{
  (e: 'setThemeColor', val: GlobalTheme): void
}>()

为 defineExpose() 标注类型

defineExpose() 类型推导直接使用参数类型自动推到即可

<script setup>
import { ref } from 'vue'

const name = ref<string>('xxx')

defineExpose({
  name
})

provide()供给一个值,可以被后代组件注入

为 provide() 标注类型, Vue 提供了一个 InjectionKey 接口,它是一个继承自 Symbol 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型

import type { InjectionKey } from 'vue'

// 建议声明 key (name) 放到公共的文件中
// 这样就可以在 inject 的时候直接导入使用
const name = Symbol() as InjectionKey<string>

provide(name, 'xxx') // 若提供的是非字符串值会导致错误

以上方式是通过定义 key 的类型来标注类型的,还有一种方式直接 key 采用字符串的形式添加

provide('name', 'xxx')

inject()注入一个由祖先组件或整个应用供给的值

provide() 的 key 的类型是声明式提供的话(provide()类型标注的第一种形式)

inject() 可以直接导入声明的 key 来获取父级组件提供的值

// 由外部导入
const name = Symbol() as InjectionKey<string>

const injectName = inject(name)

如果 provide() 的 key 直接使用的字符串形式添加的, 需要通过泛型参数声明

const injectName = inject<string>('name')

模板 ref 需要通过一个显式指定的泛型参数和一个初始值 null 来创建:

<img ref="el" class="logo" :src="Logo" alt="" />

const el = ref<HTMLImageElement | null>(null)
<!-- Child.vue -->
<script setup lang="ts">
const handleLog = () => console.log('xxx')

defineExpose({
  open
})
</script>

<!-- parent.vue -->
<script setup lang="ts">
import Child from './Child.vue'

// 为子组件 ref 声明类型
const child = ref<InstanceType<typeof Child> | null>(null)

// 调用子组件中的方法
const getChildHandleLog = () => {
  child.value?.handleLog()
}
</script>

Vue组件引入

<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
</script>

<template>
  <HelloWorld msg="Vite + Vue" />
</template>

defineProps 在有两种定义方式

const props = defineProps({
  foo: { type: String, required: true },
  bar: Number,
});

<script setup lang="ts"> 
    interface Props { 
        foo: string 
        bar?: number 
    } 
    const props = defineProps<Props>() 
</script>

默认值

// 第二种带默认值props
export interface ChildProps {
  foo: string
  bar?: number
}
const props = withDefaults(defineProps<ChildProps>(), {
   foo: "xxx"
   bar?: 3
})
<script setup lang="ts">
interface Book {
  title: string;
  author: string;
  year: number;
}

const props = defineProps<{
  book: Book;
}>();
</script>
import type { PropType } from 'vue'

interface Book {
  title: string;
  author: string;
  year: number;
}

const props = defineProps({
  book: Object as PropType<Book>
})

defineEmits和defineProps获取父组件传过来值和事件

// 第一种获取事件方法
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

// 第二种获取事件方法
const emit = defineEmits(["dosth"])

ref一般用于基本的数据类型,比如string,boolean

reactive一般用于对象

不能修改reactive设置的值

let state = reactive({ count: 0 }) 
// the above reference ({ count: 0 }) is no longer being tracked (reactivity connection // is lost!) 
// 这里state如果重新赋值以后,vue就不能双向绑定
state = reactive({ count: 1 })

useAttrs 可以获取父组件传过来的id和class等值。 useSlots 可以获得插槽的内容。

<template>

    <div class="father">{{ fatherRef }}</div>

    <Child :fatherRef="fatherRef" @changeVal="changeVal" class="btn" id="111">

        <template #test1>

        <div>1223</div>

        </template>

    </Child>

</template>

<script setup lang="ts">

import { ref } from "vue";

import Child from "./Child.vue";

const fatherRef = ref("1");

function changeVal(val: string) {

    fatherRef.value = val;

}

</script>

<style lang="scss" scoped>

.father {

    margin-top: 40px;

    margin-bottom: 40px;

}

.btn {

    font-size: 20px;

    color: red;

}

</style>


<template>

    <!-- <div class="child">{{ props.fatherRef }}</div> -->

    <div v-bind="attrs">

        <slot name="test1">11</slot>

        <input type="text" v-model="inputVal" />

    </div>

</template>

<script setup lang="ts">

import { computed, useAttrs, useSlots } from "vue";

const props = defineProps<{

    fatherRef: string;

}>();

const emits = defineEmits(["changeVal"]);

const slots = useSlots();

const attrs = useAttrs();

console.log(122, attrs, slots);

const inputVal = computed({

    get() {

        return props.fatherRef;

    },

    set(val: string) {

        emits("changeVal", val);

    },

});

</script>


<style lang="scss" scoped>

.child {

}

</style>

自定义focus指令,命名就是vMyFocus,使用的就是v-my-focus

<script setup lang="ts">
const vMyFocus = {
  onMounted: (el: HTMLInputElement) => {
    el.focus();
    // 在元素上做些操作
  },
};
</script>
<template>
  <input v-my-focus value="111" />
</template>

使用defineExpose子组件传父组件

<template>

    <div class="child"></div>

</template>


<script setup lang="ts">

import { ref, reactive } from "vue";

function doSth() {

    console.log(333);

}

defineExpose({ doSth });

</script>


<style lang="scss" scoped>

.child {

}

</style>
<template>

<div class="father" @click="doSth1">222</div>

    <Child ref="childRef"></Child>

</template>

<script setup lang="ts">

import { ref, reactive } from "vue";

import Child from "./Child.vue";

const childRef = ref();

function doSth1() {

    childRef.value.doSth();

}

</script>

<style lang="scss" scoped>

.father {

}

</style>

当从父组件向子组件传props的时候,必须使用toRefs或者toRef进行转一下 如果不使用toRefs转一次的话,当父组件中的props改变的时候,子组件如果使用了Es6的解析,会失去响应性。

解决办法

  1. 使用const { fatherRef } = toRefs(props);

  2. 在模版中中使用props.fatherRef

1. 可以在子组件中使用computed,实现双向绑定

<template>

    <div class="father">{{ fatherRef }}</div>

    <Child :fatherRef="fatherRef" @changeVal="changeVal"></Child>

</template>

<script setup lang="ts">

import { ref } from "vue";

import Child from "./Child.vue";

const fatherRef = ref("1");

function changeVal(val: string) {

    fatherRef.value = val;

}

</script>


<style lang="scss" scoped>

.father {

    margin-top: 40px;

    margin-bottom: 40px;

}

</style>

<template>

    <!-- <div class="child">{{ props.fatherRef }}</div> -->

    <input type="text" v-model="inputVal" />

</template>

<script setup lang="ts">

import { computed } from "vue";

const props = defineProps<{

    fatherRef: string;

}>();

const emits = defineEmits(["changeVal"]);


const inputVal = computed({

    get() {

        return props.fatherRef;

    },

    set(val: string) {

        emits("changeVal", val);

    },

});

</script>

<style lang="scss" scoped>

.child {

}

</style>
<template>

    <Child :modelValue="searchText" @update:modelValue="changeVal"> </Child>

</template>

<script setup lang="ts">

import { ref } from "vue";

import Child from "./Child.vue";

const searchText = ref(1);

function changeVal(val: number) {

    searchText.value = val;

}

</script>

<style lang="scss" scoped>

.father {

    margin-top: 40px;

    margin-bottom: 40px;

}

.btn {

    font-size: 20px;

    color: red;

}

</style>
<template>
  <input v-model="modelValue" />
  <Child
    :modelValue="test"
    @update:modelValue="changeTest"
    v-if="modelValue > 2"
  ></Child>
</template>

<script setup lang="ts">
import { computed, useAttrs, useSlots, ref } from "vue";
const props = defineProps<{
  modelValue: number;
}>();
const test = ref(0);
function changeTest(val: number) {
  test.value = val;
}

// const emits = defineEmits(["changeVal"]);
</script>

<style lang="scss" scoped>
.child {
  position: relative;
}
</style>

加群联系作者vx:xiaoda0423

仓库地址:https://github.com/webVueBlog/WebGuideInterview

参考资料

[1]

https://docs.docker.com/engine/install/ubuntu/: https://link.juejin.cn/?target=https%3A%2F%2Fdocs.docker.com%2Fengine%2Finstall%2Fubuntu%2F

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/463182.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mac十大必备软件排行榜 mac垃圾清理软件哪个好

刚拿到全新的mac电脑却不知道该怎么使用&#xff1f;首先应该装什么软件呢&#xff1f;如果你有同样的疑惑&#xff0c;今天这篇文章一定不要错过。接下来小编为大家介绍mac十大必备软件排行榜&#xff0c;以及mac垃圾清理软件哪个好。 一、mac十大必备软件排行榜 1.CleanMyM…

权限提升:AT || SC || PS 提权.(本地权限提升)

权限提升&#xff1a;AT || SC || PS 提权 权限提升简称提权&#xff0c;由于操作系统都是多用户操作系统&#xff0c;用户之间都有权限控制&#xff0c;比如通过 Web 漏洞拿到的是 Web 进程的权限&#xff0c;往往 Web 服务都是以一个权限很低的账号启动的&#xff0c;因此通…

电能计量管理系统在煤矿上的应用

摘要&#xff1a;随着煤矿供电系统管、控一体化的发展需要&#xff0c;本文提出了一种基于矿井光纤网络构成的煤矿电参数计量系统&#xff0c;该系统具有实现变电所各类开关、动力设备的用电高精度计量&#xff1b;远程实时监测各路电参数&#xff1b;远程抄表&#xff1b;远程…

Aspose.Pdf使用教程:在PDF文件中添加水印

Aspose.PDF 是一款高级PDF处理API&#xff0c;可以在跨平台应用程序中轻松生成&#xff0c;修改&#xff0c;转换&#xff0c;呈现&#xff0c;保护和打印文档。无需使用Adobe Acrobat。此外&#xff0c;API提供压缩选项&#xff0c;表创建和处理&#xff0c;图形和图像功能&am…

Windows环境下实现设计模式——模板方法模式(JAVA版)

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天总结一下Windows环境下如何编程实现模板方法模式&#xff08;设计模式&#xff09;。 不知道大家有没有这样的感觉&#xff0c;看了一大堆编程和设计模式的书&#xff0c;却还是很难理解设计模式&#x…

STM32驱动SG90舵机

STM32驱动SG90舵机 关于SG90舵机SG90转动角度与占空比的关系驱动SG90舵机代码①确定控制引脚②写代码 SG90舵机正常驱动现象总结 关于SG90舵机 SG90是一种小型伺服电机&#xff0c;通常用于模型制作和小型机械应用中: 问题答案SG90的工作电压是多少SG90的工作电压通常为3V至7.…

QT笔记——QtPropertyBrowser的使用

上一节&#xff0c;我们将了如何去配置QtPropertyBrowser 本节&#xff0c;我们将说明 如何 去 使用QtPropertyBrowser 这个属性类的一些基本知识 简单的几种用法&#xff1a; 首先&#xff1a; 我们需要创建一个Widget 提升一个类 为 QtTreePropertyBrowser .h文件 QtVariant…

git -团队开发 版本控制

文章目录 Git的概念Git的安装过程Git结构交互方式初始化本地仓库Git常用命令add和commit命令status命令log命令log命令2reset命令hard参数/mixed参数/soft参数 删除文件找回本地库删除的文件找回暂存区删除的文件 diff命令 分支操作分支冲突问题&#xff0c;如何解决冲突题 Git…

2023年的深度学习入门指南(9) - Triton

2023年的深度学习入门指南(9) - Triton 上一篇我们学习了如何用CUDA进行编程。 下面我们将介绍几种深度学习GPU编程的优化方法。 第一种我们称之为多面体编译器。我们知道&#xff0c;在传统的IR&#xff0c;比如LLVM-IR中&#xff0c;使用条件分支来编码控制流信息。这种相对…

Find My资讯|美国苹果AirTag市场大涨,助推Find My技术的发展

根据市场调查机构 Circana公布的最新统计数据&#xff0c;在苹果 AirTag 的助推下&#xff0c;美国市场物品追踪器市场快速发展。 报告称 AirTag 等物品追踪器已经成为旅行者的必备品&#xff0c;今年 1 月和 2 月期间&#xff0c;物品追踪器的销售额同比增长了 82%&#xff…

宁波博视眼科俞存院长:晒太阳会晒出白内障?是真的吗?

春意渐浓&#xff0c;人们纷纷踏出家门&#xff0c;享受暖暖的阳光。众所周知&#xff0c;适当晒太阳可以促进人体合成维生素D&#xff0c;对身体有一定的好处。 但你知道吗?太阳光中的紫外线可能会导致部分眼病的出现&#xff0c;例如&#xff1a;白内障。 晒太阳怎么会晒出白…

【数据结构初阶】第七节.树和二叉树的基本操作

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;Java初阶数据结构 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01;&#xff01; 文章目…

[Linux]文件系统权限与访问控制

​⭐作者介绍&#xff1a;大二本科网络工程专业在读&#xff0c;持续学习Java&#xff0c;输出优质文章 ⭐作者主页&#xff1a;逐梦苍穹 ⭐所属专栏&#xff1a;Linux基础操作。本文主要是分享一些Linux系统常用操作&#xff0c;内容主要来源是学校作业&#xff0c;分享出来的…

Docker 安装

系列文章目录 文章目录 系列文章目录前言一、Docker 安装地址&#xff1f;二、常用命令1. 基础命令2. docker 镜像命令 三、安装步骤1.卸载原有环境2.安装对应的依赖环境和镜像地址3. 安装过慢设置镜像4. 直接安装docker CE5. 启动docker服务6. 查看docker的版本7. 配置阿里云的…

改进YOLOv8 | 主干网络篇 | YOLOv8 更换骨干网络之 SwinTransformer | 《基于位移窗口的层次化视觉变换器》

论文地址:https://arxiv.org/pdf/2103.14030.pdf 代码地址:https://github.com/microsoft/Swin-Transformer 本文介绍了一种新的视觉Transformer,称为Swin Transformer,它可以作为计算机视觉通用的骨干网络。从语言到视觉的转换中,适应Transformer所面临的挑战源于两个领…

112页智慧城市大数据综合解决方案(ppt可编辑)

本资料来源公开网络&#xff0c;仅供个人学习&#xff0c;请勿商用&#xff0c;如有侵权请联系删除 项目必要性分析 完善信息基础设施布局规划&#xff0c;满足区域信息化的发展要求 信息化已成为提升城市管理、促进经济发展、改善民生的重要手段合理高效的部署宽带信息基础…

亚马逊美国站纽扣电池标准

近日&#xff0c;亚马逊美国站公布要求卖家需遵守扭电池和硬币电池的新包装和警示标签规定公告。 在亚马逊销售单独的纽扣电池和硬币电池&#xff0c;则从2023年3月2日开始&#xff0c;您需要证明您的符合儿童安全包装和警告标签要求。 适用产品有;单独的纽扣电池或硬币电池&a…

FPGA基础知识 LCMXO3LF-6900C-6BG400I FPGA可编程逻辑简介

FPGA是英文Field&#xff0d;Programmable Gate Array的缩写&#xff0c;即现场可编程门阵列&#xff0c;它是在PAL、GAL、CPLD等可编程器件的基础上进一步发展的产物。它是作为专用集成电路&#xff08;ASIC&#xff09;领域中的一种半定制电路而出现的&#xff0c;既解决了定…

ThinkPHP模型操作下

ThinkPHP模型操作下 前言1. 模型设置1.name(数据表除去前后缀的名字&#xff0c;默认是当前model的类名)2.table(完整的数据表名)3.pk 改变主键名称4.schema 设置模型对应数据表字段及类型5.disuse 数据表废弃字段&#xff08;数组&#xff09;6.模型的其他属性 2. 模型的主要功…

从零搭建MySQL监控平台(mysql-exporter+Prometheus+Grafana)

文章目录 一、软件安装二、 软件配置配置mysql_exporter配置prometheus配置Grafana 本文是我自己在Macbook上本地从零开始搭建一套MySQL监控平台&#xff0c;监控的也是我本机的MySQL&#xff0c;过程包括prometheus、mysql_exporter、Grafana的配置与下载。 一、软件安装 我是…