0%

大文件上传 第一种:不考虑使用多线程。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const inputFile = document.querySelector('input[type="file"]');

inputFile.onchange = async function(e) {
const file = e.target.files[0];
console.log('开始切片。。。')
const chunks = await cutFile(file)
console.log('结束切片',chunks)
}

//定义每份切片的大小
const CHUNK_SIZE = 1024 * 1024 * 5 //5MB


/**
* 方式1: 不考虑多线程方案
*/
async function cutFile(file) {
//一共分多少片。向上取整
const chunkCount = Math.ceil(file.size / CHUNK_SIZE)
const result = []
console.log('切片数量',chunkCount)

for(let i=0; i<chunkCount; i++) {
const chunks =await createChunk(file,i,CHUNK_SIZE)
result.push(chunks)
}
console.log('result',result)
return result

}


/**
* //接受3个参数 整个file对象 ,第几个分片,每个分片的大小
* 返回每个的分片结果
* */
function createChunk(file, index, chunkSize) {
return new Promise((resolve)=>{
//本次开始和结束切片从XX MB - XX MB
const start = index * chunkSize
const end = start + chunkSize

const fileReader = new FileReader()
//本次切片的BLOB
const blob = file.slice(start,end)
fileReader.onload = function(e) {
resolve({
start,
end,
index,
blob
})
}

fileReader.readAsArrayBuffer(blob)

})
}


大文件上传 第二种:使用多线程(worker)。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
const inputFile = document.querySelector('input[type="file"]');

inputFile.onchange = async function(e) {
const file = e.target.files[0];
console.log('开始切片。。。')
const chunks = await cutFile(file)
console.log('结束切片',chunks)

}
//定义每份切片的大小
const CHUNK_SIZE = 1024 * 1024 * 5 //5MB


/**
* 方式2:多线程方案
* 让分片任务去其他线程运算。减少浏览器卡顿时间。
*/

//看你目前电脑CPU有多少核。
//
const THREAD_COUNT = navigator.hardwareConcurrency || 4 //默认4核(最低的电脑配置)

async function cutFile(file) {
return new Promise((reslove,reject)=>{

//一共分多少片。向上取整
const chunkCount = Math.ceil(file.size / CHUNK_SIZE)
//每个线程需要分到多少个分片
const threadChunkCount = Math.ceil(chunkCount / THREAD_COUNT)

console.log('你的电脑是' + THREAD_COUNT + '核') //8
console.log('此文件一共有' + chunkCount +'个分片') //22
console.log('CPU每核需要处理' + threadChunkCount + '个分片') //3

//假射你的电脑是8核的(THREAD_COUNT)。这个8核要处理20个分片(chunkCount)
//那么我们就要让每个核处理 threadChunkCount 个分片 Math.ceil(20 / 8) = 3
// 那么到了最后一个分片 如果超出了分片总数,就取分片总数(chunkCount)

const result = []
let finishCount = 0;

for(let i=0 ; i<THREAD_COUNT; i++ ) {
//创建一个线程并分配任务
const worker = new Worker('./workers.js',{
type:"module" //设置MODULE 可以让worker.js中import 其他函数,我这里都写到一个文件了,没这么用
})

const start = i * threadChunkCount
let end = (i+1) * threadChunkCount
if(end > chunkCount) end = chunkCount;

worker.postMessage({
file,
CHUNK_SIZE,
startChunkIndex: start,
endChunkIndex: end
})

worker.onmessage = (e)=>{
//因为多线程同时进行任务。所以如果要保证返回的顺序需要指定下标
for (let i = start; i<end; i++) {
result[i] = e.data[i-start]
}

worker.terminate()
finishCount ++;


// 如果你8个线程都完事了 结束
if(finishCount == THREAD_COUNT) {

reslove(result)
}

}

}

})

}



worker.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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// import {createChunk} from './newcar.js'



function createChunk(file, index, chunkSize) {
return new Promise((resolve)=>{
//本次开始和结束切片从XX MB - XX MB
const start = index * chunkSize
const end = start + chunkSize

const fileReader = new FileReader()
//本次切片的BLOB
const blob = file.slice(start,end)
fileReader.onload = function(e) {
resolve({
start,
end,
index,
blob
})
}

fileReader.readAsArrayBuffer(blob)

})
}



onmessage = async (e) => {
const {
file,
CHUNK_SIZE,
startChunkIndex,
endChunkIndex
} = e.data

console.log({
file,
CHUNK_SIZE,
startChunkIndex,
endChunkIndex
})

const proms = []

for(let i=startChunkIndex; i < endChunkIndex; i++) {
proms.push(createChunk(file,i,CHUNK_SIZE))
}

const chunks = await Promise.all(proms)

postMessage(chunks)

}

使用页面。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<template>

<div style="display: flex; height: 100%;">
<!-- 区域1 每个区域都有一个右键选择组件 包裹着区域内容(插槽) -->
<menu-com-vue :menu="menu1" @select="selectMenu">
<div class="area1">{{label1}}</div>
</menu-com-vue>

<!-- 区域2 -->
<menu-com-vue :menu="menu2" @select="selectMenu2">
<div class="area1">{{label2}}</div>
</menu-com-vue>

</div>

</template>
<script setup>
import { ref} from 'vue'
import menuComVue from './components/menu-com.vue'


const label1 = ref('')
const label2 = ref('')
const menu1 = ref([
{lable:'部门',id:1},
{lable:'部门1',id:2},
{lable:'部门2',id:3},
{lable:'部门3',id:4},
{lable:'部门4',id:5},
])


const menu2 = ref([
{lable:'员工1',id:1},
{lable:'员工3',id:2},
{lable:'员工3',id:3},
{lable:'员工4',id:4},
{lable:'员工5',id:5},
])

const selectMenu = (item) => {
label1.value= item;
}

const selectMenu2 = (item) =>{
label2.value= item;
}

</script>

<style>
.area1 {
background: #ccc;
flex: 50% 0 0;
}

.area2 {
background-color:bisque;
flex: 50% 0 0;
}
</style>



右键菜单组件

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<template>
<div ref="containerRef" class="container">
{{ menuVis }} {{ x }}

<!--包裹内容区域的插槽-->
<slot></slot>

<!-- 使用 Teleport 将右键菜单放到BODY 因为他是设置的fiexd 如果他的父元素有transform 就不生效了-->

<Teleport to="body">
<div class="context-menu" v-if="menuVis" :style="{'top':y + 'px','left':x + 'px'}">
<ul>
<li v-for="item in menu" @click="clickItem(item)">
{{ item.lable }}
</li>
</ul>
</div>
</Teleport>

</div>
</template>

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

import useContextMenu from './useContextMenu.js'
const emits = defineEmits(['select'])

const containerRef = ref(null)

console.log('主页中onMounted',containerRef,containerRef.value)

//使用这个HOOKS 去取得当前鼠标的X,Y 以及是否显示菜单。
const {x,y,menuVis} = useContextMenu(containerRef)

// watchEffect(()=>{
// const {x,y,menuVis} = useContextMenu(containerRef.value)
// },{
// flush:'post'
// })


const props = defineProps({
// 数据源
menu: {
type: Array,
required: true
}

})



const clickItem = (item)=>{
emits('select',item.lable)
}

</script>

<style>
.context-menu {

position: fixed;
left:v-bind(x + 'px');
top:v-bind(y + 'px');
width: 200px;
height: auto;
border: 1px solid #000;
font-size: 14px;
text-align: center;
}
</style>

右键菜单HOOKS(提供鼠标位置 X,Y ,是否显示菜单)

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53

import {ref , onMounted ,onUnmounted} from 'vue'

export default function useContextMenu(container) {


console.log('use-container',container)
//菜单的位置信息 是否可见
const x = ref(0);
const y = ref(0);
const isVisible = ref(false)

//打开菜单
function showMenu(e) {

e.preventDefault();

//写这个阻止冒泡是为了那种嵌套区域的菜单 防止打开俩菜单。
e.stopPropagation()
x.value = e.pageX;
y.value = e.pageY;
isVisible.value = true;
console.log('触发右键', x.value)
}

//关闭菜单
function closeMenu() {
isVisible.value = false
}


onMounted(() => {
//关闭菜单的情况 1,点击菜单,2 打开一个新的菜单
// 先关闭所有的,再打开,需要在捕获阶段执行。否则先打开菜单 又冒泡到window上被关闭了。等于没打开菜单

window.addEventListener('contextmenu',closeMenu,true)
window.addEventListener('click',closeMenu,true)
container.value.addEventListener('contextmenu',showMenu)


})

onUnmounted(()=>{
container.value.removeEventListener('contextmenu',showMenu)
})

return {
x:x,
y:y,
menuVis:isVisible
}
}

前端使用canvas提取视频中帧的图片

首先是html

1
2
3
4
5
6
7
8
9
10
<style>
img{
width: 300px;
height: 600px;
display: block;
}
</style>

<input type="file" name="videoFile" id="videoFileInput">

调用

1
2
3
4
5
6
7
8
9
10
var inputObj = document.querySelector("#videoFileInput");
inputObj.addEventListener("change",async function (file) {
//每隔1秒创建一张图片 从0秒到4秒逐帧创建
for(var i=0; i< 5; i++) {
const frame = await captureFrame(file.target.files[0],i * 1)
//拿到图片的blob和url 就可以在页面绘制图片了
createImg(frame)
}
})

核心代码

  • 从调用方法里接受一个视频文件,创建一个视频对象。通过URL.createObjectURL 可以创建出本地的视频地址
  • 使用canvas绘制视频 并且HTMLCanvasElement.toBlob()方法可以创建blob 有了blob 通过URL.createObjectURL又可以有 URL
  • 转换链条 canvas ==> blob ==> file对象 ==>上传
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function captureFrame(vdoFile,time=0){

return new Promise((resolve, reject) => {
const vdo = document.createElement('video');
vdo.currentTime = time;
vdo.muted = true;
//由于此时浏览器没有加载视频,所以自动播放只播放当前这一帧就结束了,这样就可以让视频定格在我们指定的时间。
vdo.autoplay = true;

//通过URL.createObjectURL生成本地URL 可以传入一个file对象或者一个blob
//这个 URL 的生命周期和创建它的窗口中的 document 绑定。也就是说关掉窗口这个时候,URL 就会被释放。
//这个创建出来会是一个blob:xxxxx 放到浏览器可以直接访问,关闭掉页面窗口就销毁了
vdo.src = URL.createObjectURL(vdoFile)

//开始用canvas画视频,当视频可以播放的时候再话,因为可能视频没有加载完
vdo.addEventListener('canplay', async function () {
const frame = await drawVideo(vdo)
resolve(frame)

})

})

}


function drawVideo(vdo) {
return new Promise((resolve, reject) => {
const cvs = document.createElement('canvas');
cvs.width = vdo.videoWidth;
cvs.height = vdo.videoHeight;

const ctx = cvs.getContext('2d');
ctx.drawImage(vdo, 0, 0, cvs.width, cvs.height);

//这个方法是让canvas转换为blob 是个异步方法,所以要用promise
/**
* @returns {Promise} 1个视频的的file对象 可以上传 1个url可以用作预览
* url可以当做本地预览图片地址
* blob可以用户选中了这个图片后上传使用
*/
cvs.toBlob(blob => {
resolve({
file:new File([blob], "test.png", {type:"image/png"}),
url: URL.createObjectURL(blob)
})
})
})
}

function createImg(frame){

const img = document.createElement("img");
img.src = frame.url;
document.body.appendChild(img);
}

本地图片上传预览

  • 使用 FileReader 的 readAsDataURL 方法读取file对象,转为base64,就可以给img的src赋值了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const vdoInput = document.querySelector("#videoFileInput");

    vdoInput.addEventListener("change", async (e) => {
    const file = e.target.files[0];
    const reader = new FileReader();
    reader.onload = (e) => {
    createImg(e.target.result)
    }
    reader.readAsDataURL(file);
    })

    function createImg(data) {
    const img = document.createElement("img");
    img.src = data;
    document.body.appendChild(img);

    }

我们使用CSS变量来实现换肤是最方便的

创建CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//主题色设置 root代表默认主题色, html[data-theme]代表另一种主题色
:root{
--main-color: hotpink;
}

html[data-theme='dark'] {
--main-color: #000;
}



//主题色应用
body{
background-color: var(--main-color);
}

设置好css 在main.js中引入

我们使用hooks来创建一个theme代替使用vuex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ref, watchEffect } from "vue";

const theme = ref(localStorage.getItem("_theme_")) || ref("light");

watchEffect(() => {
document.documentElement.dataset.theme = theme.value;
localStorage.setItem("_theme_", theme.value);
});

export default function useTheme() {
return {
theme
};
}

这里的思想就是从本地缓存中读取 theme 在他发生更改的时候设置html的 data-theme属性,从而能使用到我们的第一步设置的CSS

随便写一个更改主体的组件

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
<template>
<input
:checked="theme == 'light'"
@click="changeTheme('light')"
type="radio"
id="light"
name="theme"
/><label for="light">light</label>
<input
type="radio"
:checked="theme == 'dark'"
@click="changeTheme('dark')"
id="dark"
name="theme"
/><label for="dark">dark</label>
</template>

<script setup>
import useTheme from "../useTheme.js";
const { theme } = useTheme();

const changeTheme = (params) => {
theme.value = params;
};
</script>

<style>
</style>


title: 前端零碎知识点
date: 2023-05-22 14:13:23
tags: js
category: ‘js’


使用SCSS 优化媒体查询

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
/**
使用SCSS 优化媒体查询
@content 代表使用@mixin传入的CSS
$breakpoints 代表在SASS中定义一个MAP,
$bp:map-get($breakpoints, $breakpoints );代表定义一个变量$bp,在SASS中获取这个MAP,第一个参数是这个MAP,第二个参数是这个MAP的key
$min:nth($bp, 1 ); 定义一个变量$min 取$bp这个数组的第一个值
**/

$breakpoints: (
'phone':(320px,480px),
'pad':(481px,768px),
'pc':(769px,1680px),
);

@mixin respondTo($name) {
$bp:map-get($breakpoints, $name );
$min:nth($bp, 1 );
$max:nth($bp, 2 );
@media screen and (min-width: $min) and (max-width: $max) {
@content;
}
}

.header{
width: 100%;
@include respondTo('phone'){
width: 50px;
}
@include respondTo('pad'){
width: 100px;
}
}


  • 编译结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    .header {
    width: 100%;
    }
    @media screen and (min-width: 320px) and (max-width: 480px) {
    .header {
    width: 50px;
    }
    }
    @media screen and (min-width: 481px) and (max-width: 768px) {
    .header {
    width: 100px;
    }
    }


    clip-path 裁切图片的用法 http://tools.jb51.net/code/css3path

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    .clipPath {
    width: 500px;
    height: 400px;
    }
    .clipPath img{
    width: 100%;
    height: 100%;
    transition: all 1s;
    clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
    }
    .clipPath:hover img{
    clip-path: polygon(100% 0%, 100% 100%, 0% 100%, 0% 0%);
    }


    <div class="clipPath">
    <img src="https://pic.xcar.com.cn/img/07news/23/11/15/1865fe8da72410a1d7bcb81849af78ff.jpg?imageMogr2/format/jpg/size-limit/150k!/ignore-error/1" alt="">
    </div>

IntersectionObserver 用来判断元素是否在可视范围内

html

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
    <style>
ul{
list-style: none;
display: flex;
flex-wrap: wrap;
width: 600px;
margin: 0 auto;
}
ul li {
width: 200px;
margin: 30px;
}
ul li img {
width: 100%;
height: 100%;
}
</style>
</head>
<body>

<ul>

</ul>
<script src="./IntersectionObserver.js"></script>
</body>

IntersectionObserver.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var ul = document.querySelector('ul')
var ob = new IntersectionObserver(items=>{
for(const item of items) {
// item.isIntersecting 为true 代表item出现在视窗内,false代表不在视窗内
// item.target是元素本身
console.log(item.isIntersecting,item.target)
}
})

for (var i=0; i<20; i++) {
var item = document.createElement('li');
var img = document.createElement('img')
img.src = 'https://picsum.photos/200/300?random=' + i;
item.appendChild(img)
item.dataset.index = i +1;

ul.appendChild(item);
ob.observe(item)

}

滚动到相应位置

  • 使用传统的锚点方式 会产生一个 # 如果你使用的是VUE,REACT 这种方式和HASH路由有冲突
  • 可以使用 Element.scrollIntoView()
    1
    2
    3
    4
    可以使用 Element.scrollIntoView({
    behavior:'smooth'
    })

抛物线小球 (加入购物车效果)

  • 让父元素做横向运 动,子元素做向上抛物线运动,两个运动结合就产生了购物车那种抛物线小球
  • 内部小球调整贝塞尔曲线。让他先反向再正向运动
    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
    <div class="ball">
    <div class="innerBall"></div>
    </div>

    .ball{
    position: fixed;
    width: 50px;
    height: 50px;
    border-radius: 50%;
    background-color: red;
    top: 100px;
    left: 300px;
    border: 2px solid #000;
    animation: moveX 2s linear forwards;
    z-index: 100;
    }
    .innerBall{
    width: 100%;
    height: 100%;
    border-radius: 50%;
    background-color: red;
    animation: moveY 2s cubic-bezier(0.3, -0.49, 0.32, 1.28) forwards;
    }
    @keyframes moveX {
    to{
    transform: translateX(200px);
    }
    }

    @keyframes moveY {
    to{
    transform: translateY(400px);
    }
    }

setup的子组件如果需要暴露自己的属性和方法给父组件 需要使用 defineExpose ,父组件用 在mounted中 用子组件.value可以使用

object-fit 给图片设置 cover.,,contain

background-clip 可以给文字设置透明(透明出下边的背景图)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.test {
position: relative;
width: 500px;
height: 600px;
background: linear-gradient(60deg, red, yellow, red, yellow, red);
font-weight: bold;
font-size: 50px;
background-clip: text;
-webkit-background-clip: text;
color: rgba(0,0,0,.2);
}

<div class="test">
我是文字
</div>

-webkit-text-stroke 设置文字描边

1
2
3
4
5
6
7
8
.miaobian {
-webkit-text-stroke: 2px red;
font-size: 50px;
color: #fff;
}

<p class="miaobian ">这个文字有描边</p>

使用浏览器原生的dialog

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
40
41
42
43
44
45
<button id="popBtn">弹窗</button>

<!-- <form> 元素可关闭含有属性 method="dialog" 的对话框。当提交表单时,对话框的 returnValue (en-US) 属性将会等于表单中被使用的提交按钮的 value -->
<dialog id="favDialog">
<div class="pop">
<form method="dialog">
<div>
<label for="">输入<input type="text"></label>
</div>
<div>
<label>Favorite animal:
<select>
<option value="default">Choose…</option>
<option>Brine shrimp</option>
</select>
</label>
</div>
<button id="confirmBtn">OK</button>
<!-- 不写这个favDialog.close()点取消按钮也能关闭弹窗 因为按钮在form里 -->
<button id="cancel" onclick="favDialog.close()">取消</button>
</form>
</div>
</dialog>

<script>

var popBtn = document.querySelector("#popBtn");
const favDialog = document.getElementById('favDialog');
const confirmBtn = document.getElementById("confirmBtn")
const cancelBtn = document.getElementById("cancel");

if(typeof favDialog.showModal !== 'function') {
console.log('不支持dialog')
}
popBtn.addEventListener("click",()=>{
favDialog.showModal()
})

favDialog.addEventListener('close', () => {
console.log('监听到弹窗关闭',favDialog.returnValue)
});


</script>

图片/气泡框添加阴影

  • 如果图片是SVG,并且不是矩形的(如示列中的火狐LOGO),我们添加阴影(box-shadow)的时候会是一个矩形的框,不是我们想要的
  • 如果你做了一个气泡DIV,他的小箭头 和整体要求一个阴影,但是你使用 box-shadow 也会达不到预期
  • 使用filter: drop-shadow(里边的属性和box-shadow一致);可以解决此类问题
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
html:

图片:
<img class="shadow_img" src="http://www.firefox.com.cn/media/protocol/img/logos/firefox/browser/logo-word-hor.7ff44b5b4194.svg" alt="">

气泡框:
<div class="bubble">
气泡框
</div>

css:

.shadow_img {
filter: drop-shadow(0 0 15px #000);
}

.bubble {
width: 300px;
height: 200px;
margin-left: 30px;
position: relative;
display: inline-block;
padding: 10px;
background-color: #ccc;
border-radius: 6px;
filter: drop-shadow(0 0 15px #000);
}

.bubble::before {
content: "";
position: absolute;
top: 50%;
left:-30px;
transform: translateY(-50%);
border-width: 18px;
border-style: solid;
border-color: transparent #ccc transparent transparent
}

保持元素宽高比 aspect-ratio vs 传统方案

  • 假射有一个DIV,我们需要保持他的宽高比是4:3,用新的API aspect-ratio 可以很方便的做到这点
1
2
3
4
5
6
7
8
9
10
11
<div class="aspect-ratio-div">
保持元素宽高比一直是4:3
</div>

.aspect-ratio-div {
background-color: #000;
width: 40%;
margin: 0 auto;
/*兼容性一般*/
aspect-ratio: 4 / 3;
}
  • 传统方案 使用padding
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
<div class="aspect-ratio-div">
<div class="inner">
<div class="content">这个最内部的DIV里再写元素,因为父级的DIV高度是0,是靠padding撑开的</div>
</div>
</div>



.aspect-ratio-div {
background-color: #ccc;
width: 40%;
margin: 0 auto;
}
.aspect-ratio-div .inner {
background-color: #000;
width: 100%;
height: 0;
/*这个子元素的高度要是父元素宽度的 3/4*/
/*padding 设置的百分比是相对于父元素的*/
padding-bottom: 75%;
color: #fff;
position: relative;
}
.aspect-ratio-div .inner .content {
position: absolute;
inset: 0; /*代表 top:0,left:0,bottom:0,right:0*/
}

旋转边框

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
40
41
42
43
44
45
46
47
48
.rotateBorder {
position: relative;
outline: 4px solid #000;
width: 200px;
height: 150px;
margin-left: 50px;
overflow: hidden;
box-sizing: border-box;
padding: 10px;

}
/*在他内部设置一个DIV,宽高超过父元素,一直旋转*/
.rotateBorder::before{
content: "";
position: absolute;
width: 200%;
height: 200%;
background-color: #f40;
z-index: -1; /*让他不要挡住内容*/
left: 50%;
top: 50%;
transform-origin: 0 0; /*设置旋转中心点在中间*/
animation: rotate 3s linear infinite;
}
/*再设置一个DIV,挡住旋转DIV的中间大部分,就留着旋转DIV的边部分能看到*/
.rotateBorder::after {
content: '';
position: absolute;
width: calc(100% - 20px);
height: calc(100% - 20px);
/* background-color: #ccc; */
left: 10px;
top: 10px;
background-color: #fff;
z-index: -1;
}

@keyframes rotate {
to {
transform: rotate(1turn); /*旋转1圈*/
}
}


<div class="rotateBorder">
旋转边框
</div>

使用compositionstart来解决中文输入法 实时搜索的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

const inp = document.querySelector('input');
let isComposing = false; //是否正在合成

function search() {
if(isComposing) return ;
console.log(inp.value)
}

inp.addEventListener('input', function(){
search()
})

inp.addEventListener('compositionstart', function(){
isComposing = true;
console.log('输入开始')
})

inp.addEventListener('compositionend', function(){
isComposing = false;
search()
console.log('输入结束')
})

渡一大师课

浏览器进程,线程

  • 一个进程就是开辟了一块内存空间,一个线程就是内存空间有一个干活的人。
  • 浏览器有可以有多个线程,多个进程。
  • 当前谷歌浏览器,每个TAB就是一个进程,防止互相影响。其中一个TAB崩坏也不会影响其他的

事件循环

  • 过去把消息队列分为宏队列和微队列,现在则根据不同的任务队列(主线程,微队列,延迟队列,交互队列等)分为多个队列,同类型任务在相同的队列,
    微队列有最高的优先级,主线程任务完成后优先调度微队列的任务(Promise.reslove().then(fn)直接放入微队列)

为什么使用transform做动画效率高

1
2
3
4
5
6
7
8
9
10
11
@keyframes move1 {
to {
transform:translate(100px)
}
}

@keyframes move2 {
to {
left : 100px
}
}
  • 动画都是偏移100PX 使用left 会引起重排(reflow) ,会占用浏览器的主线程,使用transform 不占用主线程 ,页面卡死也不会受影响。

处理并发请求,给你100个请求连接,每次只能同时发送3个请求,都发送完成后,返回一个结果[],里边是所有发送成功或者失败的结果

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70


/**
* 处理并发请求,控制最大并发请求数,
* @param {string[]} urls 待请求的URLS 数组
* @param {*} maxNum 最大并发请求数
* @returns {Promise[]} Promise数组 每个Promise对应一个URL的返回结果(正确或者错误都要),顺序要按照urls数组顺序
*/
function concurRequest(urls,maxNum) {
return new Promise(resolve=>{

if(urls.length == 0) {
resolve([])
}

let index = 0; //下一次请求的URL地址,在URLS数组中对应的下标
let count = 0; //请求完成的数量,这里不能使用index表示请求完成的数量,因为index只代表发送了多少次请求,不能代表完成了几次;
const result = [];
async function request() {
const i = index; //保存原数组的下标,为了放到result[]的相应位置中

const url = urls[index];
index++;
try{
//每次请求的返回结果
const resp = await fetch(url);
result[i] = resp

}catch(err){
result[i] = err;

}
finally{
count++;
console.log(count,urls.length)
if(count == urls.length) {
console.log('全部请求完成!')
//全部请求完成
resolve(result);
}

//每次无论成功还是失败 运行完都要继续调用request()
if(index < urls.length) {
request();
}

}

}

for (let i=0; i<Math.min(maxNum,urls.length); i++) {
request();
}

})
}


//使用

var urls = []
for (let i=1; i<=20; i++) {
urls.push(`https://jsonplaceholder.typicode.com/todos/${i}`)
}

concurRequest(urls,3).then(resps=>{
console.log(resps)
})


使用compositionstart来解决中文输入法 实时搜索的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

const inp = document.querySelector('input');
let isComposing = false; //是否正在合成

function search() {
if(isComposing) return ;
console.log(inp.value)
}

inp.addEventListener('input', function(){
search()
})

inp.addEventListener('compositionstart', function(){
isComposing = true;
console.log('输入开始')
})

inp.addEventListener('compositionend', function(){
isComposing = false;
search()
console.log('输入结束')
})

限制请求重试次数

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

/**
* 限制一个请求,如果请求失败的最大尝试次数
*/

function request(url,maxCount = 5) {
return fetch(url).then(res => {
console.log(res)
}).catch(err => {
if (maxCount > 0) {
return request(url,maxCount - 1)
}
else {
return Promise.reject(err)
}
})
}


request('https://adfaf.com/todos1').then(res => {

}).catch(err => {
console.log(err)
})

使用AbortController 来取消上一次请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 使用AbortController 来取消上一次请求,可用在输入框连续快速输入文字时候,每次输入都会取消上一次的请求,
* 防止请求过多,且避免返回的数据顺序错乱
*/

let controller ;
input.onInput = async () => {
// 用AbortController 来取消上一次请求
if (controller) {
controller.abort();
}
controller = new AbortController();
await fetch('https://www.baidu.com' + input.value, { signal: controller.signal });
}


vite 配置打包项目目录

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
export default defineConfig({
plugins: [vue()],

build: {
rollupOptions: {
output: {
//引入的文件放到JS下
entryFileNames:'js/[name].js',
//import 这种分包的也放到JS下
chunkFileNames:'js/[name].js',
//其他文件。包含CSS等根据自己的名字来
assetFileNames(assetInfo) {
if(assetInfo.name.endsWith('.css')) {
return 'css/[name].[ext]'
}
if(['.png','.jpg','.jpeg','.gif','.svg','.webp'].some(ext=>assetInfo.name.endsWith(ext))) {
return 'images/[name].[ext]'

}
//其余的资源
return 'assets/[name].[ext]'

}
}
}
}
})



使用CSS变量,JS获得的属性可以用于在CSS中

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
40
41
42
43
44
45
46
47
48
.cssVar {
position: relative;
width: 500px;
height: 500px;
border: 1px solid red;
}
.cssvar-ball {
/* --w:500px; */
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: calc(var(--w) - 450px); /* 50px*/
height: 50px;
border-radius: 50%;
background-color: red;
animation: moveball 3s linear infinite;
}
/*
* transfrom中的100%代表自身的宽度
*/
@keyframes moveball {
0%{
transform: translateX(0);
}
50%{
transform: translateX(calc(var(--w) - 100%));
}
100%{
transform: translateX(0);
}

}


<div class="cssVar">
<div class="cssvar-ball"></div>
</div>


/**
* 如果不知道小球的外围容器的宽度,我们需要使用JS来计算获得外围容器的宽度,然后通过CSS变量来给CSS动画计算
*/

const cssVarContainer = document.querySelector('.cssVar');
cssVarContainer.style.setProperty('--w', cssVarContainer.clientWidth + 'px');


github 地址

https://github.com/weibsgz/miniProgram

  1. 关于WXS
  • 小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。响应的数据绑定系统通过this.setData()在两者之间进行数据传输。有频繁用户交互的效果在小程序上表现是比较卡顿的。比如你在bind:touchmove事件中去不停的setData试图改变一个节点的位置,这是行不通的,太卡了。
  • 基于以上情况,微信小程序的设计者就提出了wxs的概念。 WXS 直接在渲染层做通信 性能消耗低
  • WXS 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行
  • WXS 的运行环境和其他 JavaScript 代码是隔离的,WXS 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的API。
  • WXS只能使用 JavaScript 的 ES5 语法
  • 本项目中 components/tabs 中首页滑动内容区域执行wxs的 touchstart touchend方法。
  • wxs也可以做数据格式化处理 /common/wxs/status-text.wxs
  1. 如果多个页面都要用到的公共组件 可以放到app.json中声明
1
2
3
4
5
6
"usingComponents": {
"c-icon" : "/components/icon/icon",
"c-tabs":"/components/tabs/tabs",
"c-service-preivew" : "/components/service-preview/service-preview"
},

  1. 下拉刷新,需要在目标页JSON或者全局app.json中配置enablePullDownRefresh,默认加载中的样式是白色的,需要在app.json中修改backgroundTextStyle
  1. IOS手机
    • 底部橡皮筋白色 通过在app.json中设置 backgroundColorBottom为你的背景色解决
    • 顶部橡皮筋白色 通过在业务页面的JSON中设置 backgroundColorTop解决
  1. position: sticky吸顶效果 如果他的父元素是 height:100% 那么他滚动到100%高度的时候就无效了 ,需要把父元素设置为 min-height:100%
  • fixed 生成固定定位的元素,相对于浏览器窗口进行定位。元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。
    sticky 粘性定位,该定位基于用户滚动的位置。它的行为就像 position:relative; 而当页面滚动超出目标区域时,它的表现就像 position:fixed;,它会固定在目标位置。
  1. safe-area 只有IOS10以上才有底部一个黑条 他所在的区域就是安全区域,我们的业务内容需要避开这个区域

components/safe-area/safe-area.wxss 中是处理安全区域的组件 使用不同的边距样式

  1. 外部样式类
  • 组件JS中加入配置 externalClasses: [ 'i-button-class', 'i-button-special-class' ],
  • 组件WXML中 <view wx:if="{{!special}}"class="i-button i-button-class>
  • 父组件 <c-button i-button-class="my-i-button-class">
  • 父组件 wxss .my-i-button-class {color: red !important;}
  1. 引入 WEUI
  1. 引入TIM SDK

https://cloud.tencent.com/document/product/269/37411
https://www.npmjs.com/package/tim-wx-sdk

  • 在小程序目录下 node.js > v12
    1
    2
    3
    npm init
    npm install tim-wx-sdk --save
    npm install tim-upload-plugin --save
  • 小程序编辑器中 开发者工具菜单——工具——构建 npm

model/tim.js 中初始化TIM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import TIM from 'tim-wx-sdk';
import TIMUploadPlugin from 'tim-upload-plugin';

let options = {
SDKAppID: 你申请的SDKAPPID // 接入时需要将 0 替换为您的云通信应用的 SDKAppID,类型为 Number
};
// 创建 SDK 实例,`TIM.create()`方法对于同一个 `SDKAppID` 只会返回同一份实例
let tim = TIM.create(options); // SDK 实例通常用 tim 表示

// 设置 SDK 日志输出级别,详细分级请参见 setLogLevel 接口的说明
tim.setLogLevel(0); // 普通级别,日志量较多,接入时建议使用
// tim.setLogLevel(1); // release级别,SDK 输出关键信息,生产环境时建议使用

// 注册腾讯云即时通信 IM 上传插件
tim.registerPlugin({'tim-upload-plugin': TIMUploadPlugin});

// 接下来可以通过 tim 进行事件绑定和构建 IM 应用

  • 项目中使用
1
2
3
4
import Tim from '../../model/tim'

new Tim()

  • 看到IDE的控制台打印出TIM的信息 代表引入成功

  • userSig生成 https://console.cloud.tencent.com/im/detail

  • 用sdkAppid 和 密钥生成签名 本项目中前端生成签名 (开发阶段)实际使用时候必须后端生成签名

  • 前端生成签名方法:

  • libs/tim/两个文件

  • generate-test-usersig.js 里 配置你的 用sdkAppid和密钥

  1. 安装MBOX
  • package.json

    1
    2
    3
    // 配置以下这两个依赖信息
    "mobx-miniprogram": "4.13.2",
    "mobx-miniprogram-bindings": "2.0.0"
  • 构建NPM

  • /store/tim.js 配置文件

  • page中调用

    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

    import { createStoreBindings } from "mobx-miniprogram-bindings";
    import { timStore } from "../../store/tim";


    onLoad: function (options) {
    /** mbox调用
    * this 当前页面的this
    * store
    * fields //使用store 中哪个state
    * actiion //store中用哪个action
    *
    * 配置好后 createStoreBindings 就可以把我们在store中配置的状态绑定到我们当前page对象下
    */

    this.storeBindings = createStoreBindings(this, {
    store: timStore,
    fields: ['sdkReady'],
    actions: ['login']
    })
    //当前页面的this直接可以调用store的方法了
    // this.data.sdkReady 也会有值 页面中可以使用{{sdkReady}}
    this.login()
    }

    //页面销毁时候同时 销毁STORE的绑定
    onUnload: function () {
    this.storeBindings.destroyStoreBindings()
    },

  • js中使用同样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import { createStoreBindings } from "mobx-miniprogram-bindings";
    import { timStore } from "../store/tim";

    this.storeBindings = createStoreBindings(this, {
    store: timStore,
    fields: ['sdkReady'],
    actions: { timLogout: 'logout' },
    })

    if (this.sdkReady) {
    this.timLogout()
    }


  • 在自定义组件中使用 用behaviors

    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
    import { storeBindingsBehavior } from "mobx-miniprogram-bindings";
    import { timStore } from "../../../../store/tim";

    Component({
    /**
    * 组件的属性列表
    */
    behaviors:[storeBindingsBehavior],
    storeBindings:{
    store:timStore,
    fields:['messageList'],
    actions:['getMessageList','setTargetUserId']
    },
    properties: {

    },

    /**
    * 组件的初始数据
    */
    data: {

    },

    /**
    * 组件的方法列表
    */
    methods: {

    }
    })

  1. 本项目为了测试聊天 需要 下载的腾讯小程序IM DEMO 在里边创建了一个用户weibsgz 输入我们当前这个项目的userId(微信授权登录后是2573) 和 2573聊天 在conversation中可以看到聊天
  • model/tim.js login方法里边可以知道我们的微信登录后获取接口给我们设置的用户ID (2573)

  • conversation.js中设置目标聊天人(weibsgz) 和本项目中登录的用户和weibsgz的聊天列表
    this.setTargetUserId('weibsgz')

requestAnimationFrame 和 定时器执行动画比较

  • requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,使动画和浏览器刷新频率保持一致 保持动画流畅
  • 当页面处理未激活的状态下,requestAnimationFrame将不进行重绘回流 节省CPU GPU
  • settimeout 任务被放入异步队列,只有当主线程任务执行完后才会执行队列中的任务,因此实际执行时间总是比设定时间要晚;
  • settimeout 的固定时间间隔不一定与屏幕刷新间隔时间相同,会引起丢帧。
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.moveDiv,.moveDiv1{
position: relative;
width: 50px;
height: 50px;
background-color:red;
}
.moveDiv1 {
background-color:blue;
}
</style>
</head>
<body>
<!-- 定时器和 requestAnimationFrame -->
<button id="button1">setTimeout</button>
<button id="button2">requestAnimationFrame</button>
<div class="moveDiv"></div>
<div class="moveDiv1"></div>


<script>

// // requestAnimationFrame 和 定时器执行动画比较
// // requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,使动画和浏览器刷新频率保持一致 保持动画流畅
// //当页面处理未激活的状态下,requestAnimationFrame将不进行重绘回流 节省CPU GPU

// settimeout 任务被放入异步队列,只有当主线程任务执行完后才会执行队列中的任务,因此实际执行时间总是比设定时间要晚;
// settimeout 的固定时间间隔不一定与屏幕刷新间隔时间相同,会引起丢帧。

var btn1 = document.querySelector("#button1")
var btn2 = document.querySelector("#button2")
var moveObj = document.querySelector(".moveDiv")
var moveObj1 = document.querySelector(".moveDiv1")
var timer = null;
var timer1 = null;

//setInterval执行动画
btn1.onclick = function() {
var count = 0;
if(timer) {
// clearInterval(timer)
return false
}
timer = setInterval(function(){
go(count)
count++;
if(count > 1000 ) clearInterval(timer)
},10)
}

function go(count) {
moveObj.style.transform = 'translatex(' + count + 'px)'
}



// requestAnimationFrame 执行动画
var count1 = 0;
btn2.onclick = function() {
window.cancelAnimationFrame(timer1)
timer1 = window.requestAnimationFrame(go1)
}

function go1() {

if(count1<1000) {
moveObj1.style.transform = 'translatex(' + count1 + 'px)'
count1++;
timer1 = window.requestAnimationFrame(go1)
}
}

</script>
</body>
</html>


animation 小动画

  • animation-play-state 属性指定动画是否正在运行或已暂停。可以在CSS和JS中设置
  • webkitAnimationStart-动画开始,webkitAnimationEnd-动画结束,webkitAnimationIteration-动画重复播放 ,即可以很方便的监听动画过程
    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
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>

    .warp {
    position: absolute;
    width: 100px;
    height: 300px;
    border: 1px solid #F60093;
    overflow: hidden;
    }

    .progress-in {
    position: relative;
    width: 100%;
    height: 100%;
    background: red;
    opacity: 1;
    transform: translateY(100%);
    animation: 3s linear 0s progress forwards;
    /* 动画初始状态 暂停 */
    animation-play-state:paused;
    }

    @keyframes progress {
    from {
    transform: translateY(100%);
    }
    to {
    transform: translateY(0);
    }
    }

    #start {
    position: absolute;
    width: 100px;
    height: 30px;
    left: 200px;
    }
    </style>
    </head>
    <body>

    <div class="warp">
    <div class="progress-in">

    </div>
    </div>
    <button id="start">开始</button>




    <script>

    /************** 动画 S *******************/

    const progressIn = document.querySelector(".progress-in");
    start.onclick = function () {
    progressIn.style.animationPlayState = progressIn.style.animationPlayState == `running` ? 'paused' : 'running'
    }
    //animation动画结束时监听 同理, transiation 的结束状态是 transitionend
    progressIn.addEventListener("webkitAnimationEnd", (ele) => {
    console.log(`过渡动画完成:过渡属性${ele}`);
    }, true);

    /************** 动画 E *******************/

    </script>
    </body>
    </html>

  • 我们处理一些简单的页面,不愿意使用构建工具,但是又想要使用scss和ES6语法
  • scss和js(es6)文件在 ./css , ./js 下 打包出来对应的文件在根目录下
  • 使用babel-cli命令行工具 参考文档 https://www.babeljs.cn/docs/babel-cli 安装 npm install --save-dev @babel/core @babel/cli @babel/preset-env
  • 安装concurrently 可以同时运行多个命令 不必等待第一个命令完成。
  1. package.json
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
{
"name": "y",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "concurrently \"node server.js\" \"npx babel ./js/index.js --watch --out-file ./index-es5.js --presets=@babel/preset-env\""
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.16.0",
"@babel/core": "^7.16.5",
"babel-preset-env": "^1.7.0",
"express": "^4.17.1",
"node-sass-middleware": "^0.11.0"
},
"dependencies": {
"@babel/preset-env": "^7.16.5"
}
}


  1. server.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
const express = require('express')
var path = require('path')
/**node sass */

var sassMiddleware = require('node-sass-middleware')

const app = express()

app.use(
sassMiddleware({
/* Options */
src: path.join(__dirname, 'css'),
dest: __dirname,
debug: true
})
)
app.use(express.static('./'))


app.get('/', (req, res) => {
res.sendFile(__dirname + '/' + 'index.html') //设置/ 下访问文件位置
})
//'10.200.16.100'
app.listen('3000', () => {
console.log('your1 app listening at port 3000')
})


  1. 运行npm run dev

撤回方案 git reset

  1. reset 命令的原理是根据 commitId 来恢复版本。因为每次提交都会生成一个 commitId,所以说 reset 可以帮你恢复到历史的任何一个版本。
  • 比如,要撤回到某一次提交,命令是这样:
    $ git reset --hard cc7b5be
  • git log 命令查看提交记录,可以看到 commitId 值, 这个值很长,我们取前 7 位即可。
  1. 比如你已经commit过了 这时候可能又不想commit了 比如你想修改一下提交的信息

  2. 比如说当前提交,你已经推送到了远程仓库;现在你用 reset 撤回了一次提交,此时本地 git 仓库要落后于远程仓库一个版本。此时你再 push,远程仓库会拒绝,要求你先 pull.如果你需要远程仓库也后退版本,就需要 -f 参数,强制推送,这时本地代码会覆盖远程代码。

git reset HEAD^

  • HEAD^ 表示上一个提交,可多次使用。之后再重新 git add . git commit -m '....'

git revert

  • revert 与 reset 的作用一样,都是恢复版本,但是它们两的实现方式不同。

  • 简单来说,reset 直接恢复到上一个提交,工作区代码自然也是上一个提交的代码;而 revert 是新增一个提交,但是这个提交是使用上一个提交的代码。

  • 因此,它们两恢复后的代码是一致的,区别是一个新增提交(revert),一个回退提交(reset)。

  • 正因为 revert 永远是在新增提交,因此本地仓库版本永远不可能落后于远程仓库,可以直接推送到远程仓库,故而解决了 reset 后推送需要加 -f 参数的问题,提高了安全性。

  • 说完了原理,我们再看一下使用方法:
    git revert -n [commitId]

项目地址

https://github.com/weibsgz/vue3.2.8-

菜单权限两种思路

  1. 后端返回路由表 前端做个映射 省事了
  • 简易思路
    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
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    </head>
    <body>


    <script>
    var data = [
    {
    'userManage':['user1','user2']
    },
    "roleList",
    {
    "pofile":["pro1"]
    }
    ]


    var routeObj = {
    'userManage':{
    name:'userManage',
    path:'/userManage/indexPath',
    component:"/userManage/Comindex"
    },
    "user1":{
    name:"user1",
    path:'/userManage/user1/indexPath',
    component:"/userManage/user1/Comindex"
    },
    "user2":{
    name:"user2",
    path:'/userManage/user1/indexPath',
    component:"/userManage/user1/Comindex"
    },
    "roleList":{
    name:"roleList",
    path:'/roleList/indexPath',
    component:"/roleList/Comindex"
    },
    "pofile":{
    name:"pofile",
    path:'/pofile/indexPath',
    component:"/pofile/Comindex"
    },
    "pro1":{
    name:"pro1",
    path:'/pofile/pro1/indexPath',
    component:'/pofile/pro1/Comindex'
    },
    }


    var generateRoute = function(data,routeObj) {
    var route = []

    data.map(function(v,i){
    if(Object.prototype.toString.call(v) === '[object Object]') {
    var key = Object.keys(v)[0];
    var arr = v[key];
    var obj = routeObj[key]
    obj.children = []
    for(var i=0; i<arr.length; i++) {
    obj.children.push(routeObj[arr[i]])
    }
    route.push(obj)

    }
    else {
    route.push(routeObj[v])
    }
    })


    return route
    }

    console.log( generateRoute(data,routeObj))
    </script>
    </body>
    </html>

  1. 自己根据后端返回的权限字段,动态生成路由 需要用到addRoutes
    慕课网项目 权限相关:https://www.imooc.com/wiki/vue3/elementAdmin8.html

    路由需要公有路由 和 私有路由 我们处理的是根据返回的权限 来动态插入匹配到的私有路由(addRoutes方法)

    本项目中

    • 把私有路由分成几块(根据不同权限) ./store/modules

    • 根据权限筛选路由 menus 是后端返回的权限字段 ["userManage", "roleList", "permissionList", "articleRanking", "articleCreate"]
      privateRoutes是拆分好的私有路由集合 每个私有路由上的name和 menus中的字段对应

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      filterRoutes(context, menus) {
      const routes = []
      // 路由权限匹配
      menus.forEach(key => {
      // 权限名 与 路由的 name 匹配
      routes.push(...privateRoutes.filter(item => item.name === key))
      })
      // 最后添加 不匹配路由进入 404
      routes.push({
      path: '/:catchAll(.*)',
      redirect: '/404'
      })
      context.commit('setRoutes', routes) // vuex中state.routes = [...publicRoutes, ...newRoutes]
      return routes
      }


  • src/permission.js (路由守卫) 中 使用addRoutes方法动态添加路由,最后添加404页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    // 判断用户资料是否获取
    // 若不存在用户信息,则需要获取用户信息
    if (!store.getters.hasUserInfo) {
    // 触发获取用户信息的 action,并获取用户当前权限 (通过接口返回权限菜单)
    const { permission } = await store.dispatch('user/getUserInfo')
    // 处理用户权限,筛选出需要添加的权限
    const filterRoutes = await store.dispatch(
    'permission/filterRoutes',
    permission.menus
    )
    // 利用 addRoute 循环添加
    filterRoutes.forEach(item => {
    router.addRoute(item)
    })
    // 添加完动态路由之后,需要在进行一次主动跳转
    return next(to.path)
    }
    next()


  • 退出登录时,添加的路由表并未被删除 需要退出时候重置路由表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 初始化路由表
*/
export function resetRouter() {
if (
store.getters.userInfo &&
store.getters.userInfo.permission &&
store.getters.userInfo.permission.menus
) {
const menus = store.getters.userInfo.permission.menus
menus.forEach((menu) => {
router.removeRoute(menu)
})
}