用户体验:性能,交互方式,骨架屏,反馈,需求分析等
组件库:通用表单,表格,弹窗,组件库设计,表单等
项目质量:单元测试,规范,监控,报警,monorepo等
性能优化:性能指标,代码更快,文件加载更快,框架优化,优化方案分析等
普通项目:登录注册,布局,增删改查等
研发效率:脚手架,组件库,开发规范,联调效率,自动化等
vue3+ts:vue3,vite,pinia,组件库,vue-router等
工具库:axios,工程化,工具库,pnpm,typescript等
如果直接渲染1W行列表,不出意外你的页面就要卡了,比较常见的优化方案就是虚拟滚动,就是只渲染你能看到的视窗中的几十行,然后通过监听滚动来更新这几十个dom
// 列表容器的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])
web-worker
,时间切片
,抽样Hash
三种解决方案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
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 仓库里进行项目初始化。
需要注意的有
在传统开发过程中,代码的集成工作通常是在所有工程师们工作完成后进行的,需要单独构建,这往往会花费大量的时间和精力。持续集成是一种将集成工作放在软件开发阶段的做法,以便更加有规律地构建、测试和集成代码。
持续集成可以在开发人员提交了新代码后,立即进行构建、单元测试,可以根据测试结果确定新代码或配置环境是否正确。
WeChate8e849f3d9d0a434ad212cf2ae4c8cdc.png
具体可参考官网:docs.docker.com/engine/inst…[1]
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-runer, --rm 容器推出时清理用户数据
docker run --rm -it -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register
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 # 查看公钥
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
master
为生产分支 develop
为开发分支
develop
分支下存在多个功能分支,以 develop
做为基础切出,并会合并回 develop
版本分支
下存在多个环境的版本历史,以版本控制的严格程度分化为多个子分支
master
孑然一身,只有存在紧急 Bug 时,才会有 hotfix
分支切入并合并回 master
与 develop
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()
标注类型有三种方式:
ref()
增加类型import { ref } from 'vue'
const initCode = ref<string | number>('200')
interface
然后泛型参数的形式传入import { ref } from 'vue'
interface User {
name: string
age: string | number
}
const user = ref<User>({
name:'xxx',
age: 20
})
Ref
这个类型为 ref
内的值指定一个更复杂的类型import { ref } from 'vue'
import type { Ref } from 'vue'
const initCode: Ref<string | number> = ref('200')
reactive()
返回一个对象的响应式代理。
reactive()
标注类型有两种方式:
import { reactive } from 'vue'
interface User {
name: string
age: string | number
}
const user:User = reactive({
name:"xxx",
age:'20'
})
reactive()
增加类型import { reactive } from 'vue'
interface User {
name: string
age: string | number
}
const user = reactive<User>({
name:"xxx",
age:'20'
})
computed()
标注类型有两种方式:
import { ref, computed } from 'vue'
const count = ref<number>(0)
// 推导得到的类型:ComputedRef<string>
const user = computed(() => count.value + 'xxx')
computed()
类型const user = computed<string>(() => {
// 若返回值不是 string 类型则会报错
return 'xxx'
})
为了在声明 props
选项时获得完整的类型推断支持,我们可以使用 defineProps
API,它将自动地在 script setup
中使用
const props = defineProps({
name: { type: String, required: true },
age: Number
})
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()
类型推导直接使用参数类型自动推到即可
<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>
<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>
})
// 第一种获取事件方法
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>
<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的解析,会失去响应性。
解决办法
<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>
仓库地址: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