css:
1、scoped
组件自带的样式修改需要写在style里面,不用scoped, 但是一定要带上至少两个以上类名,避免造成全局冲突
2、:class
灵活运用:class="['',(三目表达式)]"
:class="['m-container',calculation(item,index)||index===0?'maxH':'']"
className={`head ${index === 0 ? 'head0' : index === 1 ? 'head1' : 'head2'}`}
3、hover有层级嵌套时
.fatherBox{
&:hover{
.title{
color: #0e4097
}
}
}
4、opacity:0
- a.鼠标hover后 添加边框会导致元素移动,利用opacity
- b.小程序点击按钮获取手机号 按钮为图片 而只有button才有获取手机号的方法,此时可以利用一个opacity为0的div 定位在图片相同位置(z-index设置大一点),视觉点击的是图片,实际点击的是透明div c......
5、文字提示
鼠标悬浮出现系统自带的文字提示:在标签中设置 title="";title={}
6、浮动和绝对定位
浮动:
- 脱离标准文档流
- 文本环绕
- 块级元素横排显示
- 内联元素设置宽高
- 浮动元素支持 margin,但是不支持
margin: auto - 元素没有设置宽度时,宽度为内容撑开宽
- 相同点:都脱离了标准文档流
- 不同点:
- 浮动脱离了文档流,但是仍然占据内存空间,浮动的框可以向左或向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止,会对其他元素造成影响;
- 绝对定位是彻底脱离了标准文档流,不占据内存空间,不会对其他元素造成影响
浮动带来的影响: 子元素浮动,父元素的高度会塌陷。解决浮动:
- 给父级设置高度
- 父级overfolw:hidden/auto/scroll
- 父级加浮动
- 父级加伪元素
- 同级元素:clear:both
7、实现九宫格
思路:每个盒子都给它一个负边距,边距的距离恰巧就是边框的粗细,这样后面一个盒子就会"叠加"在前面那个盒子的边框上
/* 子元素 */
div {
/* 显示为网格布局 */
display: grid;
/* 子元素水平垂直居中 */
place-items: center;
/* 宽高都是100像素 */
width: 100px; height: 100px;
/* 设置盒模型 */
box-sizing: border-box;
/* 设置1像素的边框 */
border: 1px solid black;
/* 负边距 */
margin: -1px 0 0 -1px;
}
/* 第1、4、7个子元素 */
li:nth-child(3n+1) {
/* 取消左负边距 */
margin-left: 0
}
/* 前三个子元素 */
li:nth-child(-n+3) {
/* 取消上负边距 */
margin-top: 0
}
js:
1、数组操作
- 数组添加 push、unshift操作都是直接改变原数组 返回数组的长度 数组splice、pop、shift删除操作是直接改变原数组 返回被删除的元素 join返回拼接好的数组 不改变原数组 数组slice取出一部分元素 不改变原数组 concat 返回拼接的数组 不改变原数组
let arr = [1,2]
arr.join(1) // '112' 返回拼接后的字符串-不改变原数组
arr.concat(1) // [1,2,1] 返回拼接的数组-不改变原数组
arr.slice(-1) // [2] 返回被删除的元素-不改变原数组
slice(begin,end)
arr.splice(1,1) // [2] 返回被删除的元素-直接改变原数组 arr=[1]
splice(begin,deleteCount,item1,...,itmeN) deleteCount:删除的元素数量
arr.push(3) // 2 返回数组长度-直接改变原数组-数组末尾追加元素 arr=[1,3]
arr.unshift(0) // 3 返回数组长度-直接改变原数组-数组开头追加元素 arr=[0,1,3]
unshift(item1,...,itmeN)
arr.pop(-1) // 3 返回被删除的元素-直接改变原数组 此时arr=[0,1]
pop() 删除最后一项元素,返回被删除的元素。设置参数无效
arr.shift(1,1) // 0 返回被删除的元素-直接改变原数组 此时arr=[1]
shift() 删除第一项元素,返回被删除的元素。设置参数无效
- 字符串方法split
let b = '好好学习天天天向上'
b.split('天') // ['好好学习','','','向上'] 返回分割后的数组(巧计:切割什么把什么变成逗号)-不改变原字符串
b.split('天').join(1) // '好好学习000向上' 返回拼接后的字符串(巧计:拼接什么就把逗号变成什么)-不改变原数组
2、JSON
JSON的数据格式(JavaScript Object Notation) : JSON中只要涉及到字符串就必须使用双引号
- 简单值形式: 数字、字符串(必须使用双引号)、布尔值
- 对象形式:JSON中对象的属性名必须是双引号,属性值如果是字符串也必须是双引号
{
"name": "lien",
"age": 18,
"hobby":["摄影","音乐"],
"friend":"lee"
}
- 数组形式:
[1,"hi",null]
JSON.parse : 将JSON格式字符串转换为js对象(属性名没有双引号)
const user = '{"name": "Jack","gender": "男","age": 18,"major":"computer"}'
console.log(JSON.parse(user));
// 结果 user = { name:"Jack", gender:"男", age:18, major:"computer"}
const users = '[{"id":101,"name":"计算机科学"},{"id":102,"name":"软件工程"}]'
console.log(JSON.parse(users));
// 结果 users= [ {id:101,name:"计算机科学"}, {id:102,name:"软件工程"} ]
JSON.stringify : 将某个对象转换成 JSON 字符串形式 第一个参数为要传入的序列化的值,第二个参数为函数或者数组,第三个参数为文本添加缩进/空格/换行符
const userInfo= { name: 'zs', age: 20 }
console.log(JSON.stringify(userInfo));
// {"name":"zs","age":20}
3、JS普通对象转换为数组对象
const obj = { aaa: 99, bbb: 88 }
let arr = []
for (const key in obj) {
// console.log(`{${key}:${obj[key]}}`)
// {aaa:99} {bbb:88}
// arr.push(`{${key}:${obj[key]}}`)
//['{aaa:99}', '{bbb:88}']
arr.push(JSON.parse(`{"${key}":${obj[key]}}`))
// JSON.parse将数据转换为 JavaScript 对象 前提必须是JSON格式
}
console.log(arr, 'arr') // [{aaa:99},{bbb:88}]
4、indexOf 与 findIndex 区别
- indexOf:查找值作为第一个参数,采用
===比较,更多的是用于查找基本类型,如果是对象类型,则是判断是否是同一个对象的引用 - findIndex:比较函数作为第一个参数,多用于非基本类型(例如对象)的数组索引查找,或查找条件很复杂
var arr = ['a','b','c','d'];
var flag = arr.findIndex(item => {
return item === 'c';
})
console.log(flag) // 得到: 2
5、Object
- Object.keys() Object.values()
const arr=[{a:'1',b:'2'},{a:'3',b:'4'}]
console.log(Object.keys(arr)) // ['0', '1']
console.log(Object.values(arr)) // [{a: '1', b: '2'},{a: '3', b: '4'}]
const obj = {a:'1',b:'2'}
console.log(Object.keys(obj)) // ['a', 'b']
console.log(Object.values(obj)) // ['1','2']
6、Math.round()
- Math.ceil():ceil天花板,向上取整。eg:Math.ceil(11.3)=12
- Math.floor():floor地板,向下取整。eg:Math.floor(11.6)=11
- Math.round():四舍五入。eg:Math.round(11.5)=12
7、生成随机数
要求:随机生成a,b区间的n个整数,且不能重复
8、replace
- 完全匹配(substr)
let str="Visit Microsoft Microsoft!";
str = str.replace("Microsoft","W3School");
console.log(str); // Visit W3School Microsoft!
- 正则式匹配(regexp)
1. 直接语法:
/pattern/attributes
pattern: 我们最常要编辑的正则表达式
attributes: 是一个可选的字符串,包含属性 "g"、"i" 和 "m",分别用于指定全局匹配、忽略区分大小写的匹配和多行匹配。可以多个同时使用,例如/gi
//增加/i时,忽略大小写
let str="Visit Microsoft microsoft!";
str = str.replace(/Microsoft/gi,"W3School");
console.log(str); // 输出Visit W3School W3School!
9、parseFloat
可解析一个字符串,并返回一个浮点数。开头和结尾的空格是允许的。如果字符串的第一个字符不能被转换为数字,那么 parseFloat() 会返回 NaN。只想解析数字的整数部分,请使用 parseInt() 方法
parseFloat(10.33) // 10.33
parseFloat("10.33") // 10.33
parseFloat("10.33%") // 10.33
parseFloat("10 33") // 10
parseFloat("he was 40") // NaN
vue:
1. nextTick
2. {{}}里面可以含带return的函数
3. api请求
无论是在created还是mounted中,都会等函数执行完毕,api请求才会结束。放在created中可以提前触发请求,只要代码清晰,放在created也可。 为了避免产生两个分支混乱 推荐把请求放在mounted中
- ①api放在created中 产生两个分支混乱: created=>api请求=>(mounted之后 请求结束 有了数据)导致组件重新渲染 =>mounted=>组件首次渲染
- ②mounted中 created=>mounted=>组件首次渲染=>api请求=>组件重新渲染
4、vuex中防止页面刷新使用的vuex数据丢失
配置vuex-persistedstate插件。指定配置持久化的state:
plugins: [createPersistedState({
reducer (val) {
return {
// 只储存state中的userInfo
userInfo: val.userInfo
}
}
})],
5. keepAlive
- ①在App设置
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
- ②在A页面的路由设置 meta: { keepAlive: true } 保证从A->B缓存数据
- ③在页面A中:
beforeRouteLeave (to, from, next) {
// 从A页去到别的页面,如果不是B,则不缓存A页
if (to.name !== 'B的路径') {
this.$route.meta.keepAlive = false
}else {
this.$route.meta.keepAlive = true
}
next()
}
6. 组件属性的冒号:
- 加冒号的,说明后面的是一个变量或者表达式
- 没加冒号的后面就是对应的字符串字面量
7. v-print打印:
① 给要打印的部分设置一个 id
② 在打印按钮中添加 v-print="'#id名'"
8. pdf预览
1、安装vue-pdf(参考xihupc)
// 使用
<PDFViewer :url="`${$imgUrl}${detail.content}`" />
// 封装成组件
<!-- 页面 -->
<template>
<div class="wrapper">
<template v-for="i in numPages">
<pdf :key="i" ref="pdf" :src="pdfSrc" :page="i"></pdf>
</template>
</div>
</template>
<script>
import pdf from "vue-pdf"
export default {
name: "PDFViewer",
components: {
pdf,
},
props: {
url: {
type: String,
required: true,
},
},
data() {
return {
numPages: 1,
pdfSrc: "",
}
},
computed: {},
watch: {},
created() {},
mounted() {
this.getPDFNumbers(this.url)
},
methods: {
getPDFNumbers(url) {
this.pdfSrc = pdf.createLoadingTask(url)
this.pdfSrc.promise
.then((pdf) => {
this.numPages = pdf.numPages
})
.catch((err) => {
console.error("pdf加载失败", err.messsage)
})
},
},
}
</script>
<style lang="scss" scoped>
//@import url(); 引入公共css类
</style>
2、安装react-pdf(参考xihu-admin/feat-previewPdf),需要node>16
// 使用
<PreviewPDF content={content} />
// 封装成组件
import React, { useState } from 'react';
import configSetting from '../../../config/defaultSettings';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
// 配置PDF.js工作程序
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.js',
import.meta.url,
).toString();
const PreviewPDF: React.FC<any> = (props: any) => {
const { content } = props; // pdf-url
const [numPages, setNumPages] = useState<any>();
const [pageNumber, setPageNumber] = useState<number>(1);
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
setNumPages(numPages);
};
const renderOtherPDF = (nums: number) => {
return new Array(nums)?.fill(1)?.map((e, i: number) => {
if (i > 0) {
return (
<Document
key={i}
file={configSetting.target + content}
onLoadSuccess={onDocumentLoadSuccess}
>
<Page pageNumber={i + 1} />
<p style={{ textAlign: 'center' }}>
Page {i + 1} of {numPages},共{numPages}页
</p>
</Document>
);
}
});
};
return (
<div
className="pdfBox"
style={{ width: '100%', height: '68vh', overflowX: 'hidden' }}
>
{/* 样式有待调整,react-pdf需要node16+ */}
<Document
file={configSetting.target + content}
onLoadSuccess={onDocumentLoadSuccess}
>
<Page pageNumber={pageNumber} />
<p style={{ textAlign: 'center' }}>
Page {1} of {numPages},共{numPages}页
</p>
</Document>
{
numPages > 0 && renderOtherPDF(numPages) //这里显示除了第一张PDF,剩下所有的PDF
}
</div>
);
};
export default PreviewPDF;
3、利用浏览器自带的预览pdf(参考西湖admin),注意如果后端未处理响应头,返回的流文件有可能会导致前端直接访问变成下载,解决办法参照react:8、检验pdf文件是否破损
<iframe
allow="fullscreen; encrypted-media; pdf"
id="preview"
src={configSetting.target + pdfUrl}
width="100%"
height="500px"
/>
9. 骨架屏
//推荐封装成组件(参考西湖pc)
<el-skeleton :loading="loading" animated style="margin-top: 20px">
<template slot="template">
<template v-for="i in num">
<el-skeleton-item :key="i" variant="txt" style="height: 20px; margin-top: 10px" />
</template>
</template>
</el-skeleton>
//使用
<Skeleton :loading="!workArticleScrollList.length" :num="4" style="width: 100%; margin-top: 10px" />
10. vue全局变量:包含全局css
在main.js中定义
// 图片url前缀
Vue.prototype.$imgUrl =
process.env.NODE_ENV === "development"
? process.env.VUE_APP_IMAGE_HOST
: `${window.location.protocol}//${window.location.hostname}:9000`
//全局的盒子阴影
Vue.prototype.$customShadow = customShadow
使用
<PDFViewer v-else :url="`${$imgUrl}${detail.content}`" />
// 注入--custom-shadow,使得当前元素里面的子元素都可使用
<div class="mediaBox" :style="scssVars"></div>
//定义--custom-shadow:使用全局$customShadow
scssVars() {
return { "--custom-shadow": this.$customShadow && this.$customShadow.shadow2 }
},
// 子元素使用
mediaBoxSon:hover {
box-shadow: var(--custom-shadow);
}
11. vue配置环境变量
- 配置开发环境变量:.env.development文件
VUE_APP_IMAGE_HOST = https://xxx.xxx.com
VUE_APP_LOGIN_IFRAME_SRC = http://xxx:8000/login
VUE_APP_API_HOST = http://xxx:8080/api
- 配置生产环境变量:.env.production文件
VUE_APP_IMAGE_HOST=http://xxx:9000
VUE_APP_API_HOST=http://xxx:8000/api
VUE_APP_LOGIN_IFRAME_SRC=http://xxx:8088/login
- 如遇到在不同环境部署时以上生产环境变量不采用。当接口请求路径和当前部署的环境的路径一致时,采用变量:
const apiHost =
process.env.NODE_ENV === "development"
? process.env.VUE_APP_API_HOST
: `${window.location.protocol}//${window.location.hostname}/api`
const instance = axios.create({
method: "POST",
timeout: 30 * 1000,
baseURL: apiHost,
})
12. vue官网/h5做适配
- 安装
postcss-px-to-viewport插件
npm i postcss-px-to-viewport
2. 在项目根目录下添加.postcssrc.js文件
module.exports = {
plugins: {
"postcss-px-to-viewport": {
unitToConvert: "px", // 要转化的单位
viewportWidth: 1920, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: "vw", // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
exclude: [
/host/,
/fixedColumn/,
/paginationList/,
/footBox/,
], // 设置忽略文件,用正则做目录名匹配
include: undefined, // 如果设置了include,那将只有匹配到的文件才会被转换
landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
},
},
}
3. 注意:h5关于兼容第三方UI库如vant,vant团队的是根据375px的设计稿去做的,理想视口宽度为375px,UI设计稿宽度为750px
module.exports = ({ file }) => {
const designWidth = file.dirname.includes('node_modules/vant') ? 375 : 750;
return {
plugins: {
autoprefixer: {},
"postcss-px-to-viewport": {
unitToConvert: "px",
viewportWidth: designWidth,
unitPrecision: 6,
propList: ["*"],
viewportUnit: "vw",
fontViewportUnit: "vw",
selectorBlackList: [],
minPixelValue: 1,
mediaQuery: true,
exclude: [],
landscape: false
}
}
}
}
13.vue-carousel-3d
适用场景:3D轮播 wlada.github.io/vue-carouse…
1. Install
npm install -S vue-carousel-3d
2. Usage(global)
import Vue from "vue"
import Carousel3d from "vue-carousel-3d"
Vue.use(Carousel3d)
3. Usage(local)
import { Carousel3d, Slide } from "vue-carousel-3d"
export default {
components:{
Carousel3d,
Slide
}
}
4. HTML
// 数据为空时用骨架屏提升用户体验
<template v-if="!homeCarouselList.length">
<el-skeleton :loading="!homeCarouselList.length" animated>
<template slot="template">
<el-skeleton-item variant="image" style="width: 600px; height: 400px" />
</template>
</el-skeleton>
</template>
<template v-else>
// 渲染 Carousel3d
<Carousel3d
:autoplay="true" // 自动轮播
:autoplayTimeout="3000" // 轮播时间间隔
:perspective="35" // 倾斜角度
:scaling="100" // 后面的slide缩放的大小
:display="homeCarouselList.length > 3 ? 3 : homeCarouselList.length" // 视图一行显示多少slide
:border="1"
:animationSpeed="1000" // 速度
:width="600" // 每个slide的宽高
:height="400"
:space="300" // 每个slide之间的间隔
:controlsVisible="false" // 不显示控制按钮
>
<slide v-for="(item, i) in homeCarouselList" :index="i" :key="i">
<template>
<div style="height: 100%" @click="handleDetail(item)">
<div style="height: 100%">
<ImageBox :fit="'cover'" :url="item.src" />
</div>
<div class="bgBox">
<p :class="[$isIEShow ? 'titleIE' : 'title']">{{ item.title }}</p>
</div>
</div>
</template>
</slide>
</Carousel3d>
</template>
5.CSS
.carousel-3d-container {
width: 64% !important;
}
.carousel-3d-slide {
box-shadow: 4px 0px 16px 0px rgba(0, 0, 0, 0.08);
border: 1px solid #ffffff;
overflow: hidden;
border-radius: 4px;
background: transparent;
/* border: none; */
}
.carousel-3d-slide .bgBox {
width: 100%;
z-index: 999;
color: #fff;
position: absolute;
bottom: 0;
left: 0;
font-size: 16px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
background: linear-gradient(180deg, rgba(71, 71, 71, 0) 0%, rgba(0, 0, 0, 0.84) 100%);
}
14.swiper
使用场景:以下示例是平面轮播且slide小于三个(大于三个简易的可直接用UI库的轮播,比较复杂的查看swiper demo);swiper功能强大可3D 3.swiper.com.cn/api/start/2…
import Swiper from "swiper"
import "swiper/dist/css/swiper.css"
<template>
<div class="swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item, index) in imgList" :key="index" @click="handleOpenUrl(item)">
<img class="contentImg" :src="item.img" alt="" />
</div>
</div>
<div class="swiper-pagination" />
</div>
</template>
export default {
...
mounted(){
this.swiper = new Swiper(".swiper-container", {
pagination: ".swiper-pagination", // 分页器
paginationClickable: true, // 点击切换
loop: true, // 循环
// spaceBetween: 20, // swiper-slide 间的距离
autoplay: 8000, // 8s切换一次
speed: 1000, // 每次切换时长
})
}
}
15.搜索'xxx'让列表里包含'xxx'关键字的标题和内容的'xxx'高亮
a. split()
const a = '好好学习天天向上'
const b = a.split('天天')
const c = a.split('天')
console.log(a,b)
// a:'好好学习天天向上'
// b:['好好学习', '向上']
// c:['好好学习', '', '向上']
b. join()
const arr = ['','aaa','']
const str = arr.join('b')
console.log(arr,str)
// arr: ['','aaa',''] ; str: 'baaab'
- 内容列表:legalityList,搜索关键字:searchVal
- 遍历出列表里面所有的title、content分别组成两个数组1.2
- 拿到需要高亮的两个dom节点
- 在数组1中循环每个子项title,查找是否包含searchVal,如果包含,将子项去除关键字之后的数组用join拼接含有高亮样式的span成新的字符串,赋值给对应下标的dom节点
const titleList = this.legalityList?.map((item)=> item?.title)
const contentList = this.legalityList?.map((item)=> item?.content)
// 获取title\content的dom节点
const titleDom = document.getElementsByClassName("title_style")
const contentDom = document.getElementsByClassName("content_style")
this.screenSearch(titleDom, titleList)
this.screenSearch(contentDom, contentList)
screenSearch(name, list) {
const searchVal = this.searchVal
// 遍历所有对话文本内容
for (let i = 0; i < list.length; i++) {
// 当对话内容中有包含搜索框中的字符串时
if (list[i].indexOf(searchVal) >= 0) {
// 先将包含关键字的对话内容去除关键字拆分为数组
const titleVal = list[i].split(searchVal)
// 然后再以一段设置了css样式的标签为分隔符,将数组拼接为字符串
// 再赋值给对应的dom,让其节点的innerhtml为这个字符串
name[i].innerHTML = titleVal.join('<span style="color:#0E4097 ;">' + searchVal + "</span>")
}
}
},
16.限制IP访问某个模块详情
- 官网首页后端返回本机IP
this.$store.dispatch("keyWork/getIps", {}).then((res) => {
localStorage.setItem("nativeIP", res)
})
2. 点击详情判断本机IP是否在可允许的IP范围内
handleKeyWorkView(e) {
// 重点工作 可查看IP范围:41.199.1.1 - 41.199.63.255
if (process.env.NODE_ENV === "development") {
this.handleDetail(e)
return
}
const arr = localStorage.nativeIP && localStorage.nativeIP.split(".")
if (arr[0] === "41" && arr[1] === "199" && Number(arr[2]) >= 1 && Number(arr[2]) <= 63) {
this.handleDetail(e)
} else {
this.$notify({
title: "警告",
message: "当前IP不在可访问范围内",
type: "warning",
})
}
},
17.h5实现VR 照片全景查看器
- 引入photo-sphere-viewer
yarn add photo-sphere-viewer
npm install photo-sphere-viewer
2. 使用
import { Viewer } from 'photo-sphere-viewer'
import MarkersPlugins from 'photo-sphere-viewer/dist/plugins/markers'
import { GyroscopePlugin } from 'photo-sphere-viewer/dist/plugins/gyroscope'
import 'photo-sphere-viewer/dist/photo-sphere-viewer.css'
import 'photo-sphere-viewer/dist/plugins/markers.css'
const viewer = new Viewer({
defaultZoomLvl: 0,初始缩放级别(0-100)
container: document.querySelector('#map'), // 必填,全景图容器
panorama: this.url + this.vrObj[this.list[0]?.id], //必填:全景图图片路径
loadingImg: require('../../assets/photosphere-logo.gif'), // 加载圆圈的图片路径,用来控制全景图的移动
autorotateDelay: 2000, // 自动旋转开始后的延迟
plugins: [
GyroscopePlugin, // 插件将添加一个新的“陀螺仪”按钮
[
MarkersPlugins, // 给全景图添加标记
{
markers
}
]
]
})
18.官网首页IntersectionObserver实现懒加载
- IntersectionObserver用法:callback接收一个回调函数;option是配置项。
Intersection Observer API提供了一种异步检测目标元素与祖先元素或视口(可统称为根元素)相交情况变化的方法。
// 创建实例
const observer = new IntersectionObserver(callback, option);
// 开始观察element1
observer.observe(element1);
// 开始观察element2
observer.observe(element2);
// 停止观察
observer.unobserve(element);
// 关闭观察器
observer.disconnect()
2. 创建mixin,提供一个全局方法observeElement
// 创建一个 mixin
const lazyFetchMixin = {
methods: {
observeElement(element, callback) {
if (!IntersectionObserver) {
callback()
} else {
const observer = new IntersectionObserver(
(entries, observer) => {
// entries包含n个IntersectionObserverEntry对象
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 当元素进入视口时,执行回调函数
callback()
// 然后停止观察这个元素
observer.unobserve(entry.target)
}
})
},
{
root: null, // 使用视口作为根元素,默认为浏览器视口
rootMargin: "0px", // 根元素的边距
threshold: 0.05, // 当元素的 5% 进入视口时触发回调。取值范围0-1 数组:到达某个阈值就执行一次
}
)
// 开始观察元素
observer.observe(element)
}
},
},
}
export default lazyFetchMixin
- 使用mixin
<template>
<div class="energyHome" ref="energyHomeModule"></div>
</template>
mounted() {
this.observeElement(this.$refs.energyHomeModule, () => {
this.$store.dispatch("threeYear/getThreeYearNews", { params: { type: 8, pageNum: 1, pageSize: 8 } })
})
},
19.音频和字幕同步加载
- 借用类似LRC文件编辑器将音频里对应时间轴的字幕提取出来
- 监听播放器的timeupdate事件,获取当前audio.currentTime。遍历歌词数组,在字幕数组中查找与当前时间匹配的字幕
- 把匹配的字幕更新。匹配规则:当前遍历的歌词时间轴小于或等于播放器的当前时间,且下一句歌词的时间轴大于播放器的当前时间。
// 监听播放器的timeupdate事件
audioPlayer.addEventListener('timeupdate', function() {
// 获取当前播放的时间
var currentTime = audioPlayer.currentTime;
// 根据当前时间匹配对应的歌词
var currentLyric = findLyricByTime(currentTime);
// 更新显示当前歌词
displayLyric(currentLyric);
});
// 根据当前时间匹配对应的歌词
function findLyricByTime(currentTime) {
// 在歌词数组中查找与当前时间匹配的歌词
for (var i = 0; i < lyrics.length; i++) {
if (lyrics[i].time <= currentTime && (!lyrics[i+1] || lyrics[i+1].time > currentTime)) {
return lyrics[i];
}
}
return null;
}
react:
1. useEffect(()=>{},[常量])
用常量的作用:让此函数只在页面加载的时候调用一次
2. react中使用echarts图表加上事件后一直闪烁?
- 使用useMemo缓存事件函数,
<ReactECharts
style={{ width: '128px', height: '128px' }}
echarts={echarts}
lazyUpdate={true}
option={getOption2()}
onEvents={{ mouseover: handleEvents }}
/>
const handleChartsEvents = (val: any) => {
const sp: any = document.getElementById('initmore');
sp.style.display = 'none';
setMoreContent(
<div id="more">
<div
className={`top ${
val.data.name == '转码率(绿码)'
? 'topGreen'
: val.data.name == '转码率(红码)'
? 'topRed'
: 'top'
}`}
>
{val.percent}
<span>%</span>
</div>
<div className="bottom">{val.data.name}</div>
</div>,
);
};
const handleEvents = useMemo(() => handleChartsEvents, []);
3. 解决ref为字符串产生效率问题?
- ref为字符串:refs里包含了整个实例中所有的ref,使用时逐一解构,产生效率问题。简单,只需给ref传个名字,就会包含在refs里面
- 使用回调形式的ref:
(a)把函数体写在ref回调函数里(内联函数):把当前节点挂载到当前组件实例。react渲染的时候会自动调用,且调用两次(无影响)。第一次为Null,第二次才有真正的节点。
(b)使用类绑定的回调(与(a)一样)
- 使用createRef API:createRef调用后可以返回一个容器,该容器存储被ref所标识的节点,无需自己绑定。(推荐使用)
- 函数组件没有实例!
- 尽可能避免使用ref。如此案例中:当事件触发对象为对象本身时,使用even.target就可以获得自身节点。或者利用onChange事件将组件变为受控组件。
- 何时使用ref:(a)管理焦点,文本选择或媒体播放.(b)触发强制动画.(c)集成第三方 DOM 库
4.react生命周期
- 旧
- 新
5.excel文件导入
安装插件 xlsx
// 通过FileReader对象读取文件
const fileReader = new FileReader();
// 以二进制方式打开文件
fileReader.readAsBinaryString(file.file);
fileReader.onload = (event: any) => { // 异步操作,excel文件加载完成以后触发
try {
const { result } = event.target;
// 以二进制流方式读取得到整份excel表格对象,cellDates 参数设置为 true 来启用日期转换,并使用 dateNF 参数来指定日期格式 dateNF: "yyyy-mm-dd"
const workbook = XLSX.read(result, {
type: "binary",
cellDates: true,
dateNF: "yyyy-mm-dd",
});
// 存储获取到的数据
let data: any = {};
// 遍历每张工作表进行读取(这里默认只读取第一张表)
for (const sheet in workbook.Sheets) {
let tempData: any = [];
if (workbook.Sheets.hasOwnProperty(sheet)) {
// 利用 sheet_to_json 方法将 excel 转成 json 数据
data[sheet] = tempData.concat(
XLSX.utils.sheet_to_json(workbook.Sheets[sheet])
);
}
}
const excelData = data.Sheet1
} catch (e) {
// 这里可以抛出文件类型错误不正确的相关提示
message.error("文件类型不正确!");
}
}
6.下载文件
- a链接下载:模拟a链接的点击,把后端返回的下载地址(或者本地的文件)设置给a链接的href属性。优点:可以直接下载txt、png、pdf、exe、xlsx等类型文件;缺点:a标签只能做get请求,所以url有长度限制、无法在header中携带token做鉴权操作、无法判断接口是否成功、跨域限制
const templateUrl = require("./template.xlsx");
downFile(window.location.origin + templateUrl.default, "预警模板");
// 下载文件,自定义文件名称
export function downFile(url: any, fileName: any) {
const x = new XMLHttpRequest();
x.open("GET", url, true);
x.responseType = "blob";
x.onload = function () {
const url = window.URL.createObjectURL(x.response);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
};
x.send();
}
/**
*如果你想要实现下载文件时保持文件名一致
*可以通过使用XMLHttpRequest来获取文件的二进制数据
*然后创建一个Blob对象,并使用URL.createObjectURL()生成一个临时的URL
*最后将这个URL赋值给a标签的href属性进行下载
* @param url 文件地址 数据库返回的路由
* @param filename 下载出的文件名
*/
const handleDownload = (url: any, filename: string) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `/api/openApi/download?filePath=${url}`, true);
xhr.responseType = 'blob';
xhr.onload = function () {
if (xhr.status === 200) {
const blob = new Blob([xhr.response], {
type: 'application/octet-stream', //告知浏览器这是一个字节流,浏览器处理字节流的默认方式就是下载
});
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl);
}
};
xhr.send();
};
//使用:appendixUrl不带域名。格式为:/data/oss/image/202307/52054b03-bb2a-400f-b6fc-e0de2acd4a39.jpg
handleDownload(item?.appendixUrl, item?.appendixName)
<a :href="getImageUrl(item.appendixUrl)" download>{{ item.appendixName }}</a>
- window.open下载:
window.open(url, '_self'); //_self当前窗口显示目标网页;_blank新窗口
7.react配置环境变量
- 配置开发环境变量
define: {
'process.env.API_HOST': '', // dev 本地开发环境,为空是因为本地开发实际走的代理,设置proxy即可
'process.env.PKI_HOST': 'https://xxx:8443',
},
proxy: proxy['dev'],
dev: {
'/api': {
target: 'http://xxx:8080', // xxx地址
pathRewrite: { '^': '' }, // 联调备用不关机IP 需要api
changeOrigin: true,
},
// pki接口不要使用代理,要直接请求
'/pki': {
target: 'https://xxx:8443',
changeOrigin: true,
pathRewrite: { '^/pki': '' },
secure: false,
},
},
- 配置生产环境变量
define: {
// 目前正式环境的API_HOST、PKI_HOST随着部署地址变化,并非固定地址。
'process.env.API_HOST': 'http://xxx:8088',
'process.env.PKI_HOST': 'https://xxx:8443',
},
- 不同环境部署时环境变量
// 请求api
const isProd = process.env.NODE_ENV === 'production';
const { protocol, hostname } = window.location;
newUrl = `${
isProd ? `${protocol}//${hostname}:8088` : process.env.API_HOST
}${url}`;
// 图片
const isProd = process.env.NODE_ENV === 'production';
export default {
target: isProd
? `${global?.location?.protocol}//${global?.location?.hostname}:9000`
: 'https://xxx.xxx.com', // 数据库图片存储格式只返回路由,需要自行拼接
};
8.检验pdf文件是否破损、Blob对象转换成可支持本地预览的url(解决读取pdf链接直接成为下载pdf问题)
- 使用FileReader将上传的PDF文件读取为ArrayBuffer
- 创建一个DataView来读取ArrayBuffer中的数据
- 并检查PDF文件的魔数是否匹配(PDF文件的魔数是文件开头的四个字节,表示文件的类型。对于PDF文件来说,魔数应为0x25504446(即"%PDF"的十六进制表示))
- 如果魔数匹配,则认为文件没有破损;否则,认为文件破损
const pdfUploadProps = {
maxCount:1,
onRemove:()=> {
setPdfFile([])
setPdfUrl('')
},
beforeUpload:(file):any=> {
if (file.type !== 'application/pdf') {
return message.error('上传文件非pdf格式, 请重新选择');
}
// 通过 Blob对象转换file
const blob = new Blob([file], {
type: 'application/pdf',
});
const pdfUrl = URL.createObjectURL(blob);
// pdfUrl可以作为内嵌iframe中的src,解决读取pdf链接直接成为下载pdf问题
setPDFurlBlob(pdfUrl);
setPdfBlob(true); // 区别于不需要转换url可以直接展示后端返回的链接的地方
// 使用FileReader将上传的PDF文件读取为ArrayBuffer
const reader = new FileReader();
// 当文件加载完成时
reader.onloadend = (e) => {
const arrayBuffer: any = e.target?.result;
// 创建一个新的DataView以便读取arrayBuffer
const dataView = new DataView(arrayBuffer);
// 检查PDF文件的魔数是否匹配
const magicNumber = dataView.getUint32(0, false);
if (magicNumber === 0x25504446) {
setPDFUploading(true);
const formData = new FormData();
formData.append('files', file);
upload(formData)
.then((res) => {
setPDFFile([file]);
setPdfUrl(res.result[0] + '?name=' + file.name);
})
.catch((e) => {
console.log(e);
})
.finally(() => {
setPDFUploading(false);
});
} else {
message.error('PDF文件破损。');
}
};
reader.onerror = () => {
console.log('Failed to read file.');
};
// 读取文件为ArrayBuffer
reader.readAsArrayBuffer(file);
},
fileList:pdfFile
}
// 抽离
function checkUploadedPDF(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
// 当文件加载完成时
reader.onload = function(event) {
const arrayBuffer = event.target.result;
// 创建一个新的DataView以便读取ArrayBuffer
const dataView = new DataView(arrayBuffer);
// 检查PDF文件的魔数是否匹配
const magicNumber = dataView.getUint32(0, false);
if (magicNumber === 0x25504446) {
resolve('PDF文件没有破损。');
} else {
reject('PDF文件破损。');
}
};
// 读取文件为ArrayBuffer
reader.readAsArrayBuffer(file);
});
}
9. react大屏做适配
媒体查询百分比布局栅格系统缩放(优点:简单,代码量少;缺点:1、此方式是按照UI稿等比缩放,当展示的大屏和UI稿很不一样时,会出现周边留白的情况;2、缩放比例过大时,字体图片会出现失真,甚至事件热区会偏移)
scale原理
scale 方案是通过 css 的 transform 的 scale 属性来进行一个 等比例缩放 来实现屏幕适配的,既然如此我们要知道以下几个前提:
- 设设计稿的 宽高比 为 a,则在任意显示屏中,只要展示内容的容器的 宽高比 也是 a,则二者为 1:1 只要 等比缩放/放大 就可以做到完美展示并且没有任何白边
- 如果设计稿的 宽高比 为 a, 而展示容器 宽高比 不是
a的时候,则存在两种情况。- 宽高比大于 a,此时宽度过长,计算时基准值采用高度,计算出维持 a 宽高比的宽度。
- 宽高比小于 a,此时高度过长,计算时基准值采用宽度,计算出维持 a 宽高比的高度。
- 使用的时候将下面函数定时执行,多久执行一次。启动监听窗口变化去执行这个定时器
- 可直接下载别的写好的插件:更优化的方案看github.com/Alfred-Skyb…
const el = document.querySelector('#xxx')
// * 需保持的比例
const baseProportion = parseFloat((width / height).toFixed(5))
// * 当前屏幕宽高比
const currentRate = parseFloat((window.innerWidth / window.innerHeight).toFixed(5))
const scale = {
widthRatio: 1,
heightRatio: 1,
}
// 宽高比大,宽度过长
if(currentRate > baseProportion) {
// 求出维持比例需要的宽度,进行计算得出宽度对应比例
scale.widthRatio = parseFloat(((window.innerHeight * baseProportion) / baseWidth).toFixed(5))
// 得出高度对应比例
scale.heightRatio = parseFloat((window.innerHeight / baseHeight).toFixed(5))
}
// 宽高比小,高度过长
else {
// 求出维持比例需要的高度,进行计算得出高度对应比例
scale.heightRatio = parseFloat(((window.innerWidth / baseProportion) / baseHeight).toFixed(5))
// 得出宽度比例
scale.widthRatio = parseFloat((window.innerWidth / baseWidth).toFixed(5))
}
// 设置等比缩放或者放大
el.style.transform = `scale(${scale.widthRatio}, ${scale.heightRatio})`
- 安装
r-scale-screen(毕竟是别人写的插件,不能保证一直能用,github.com/Lian-echo/r…
yarn add r-scale-screen
import RScaleScreen from "r-scale-screen";
<div className={styles.screenBox}>
<RScaleScreen height={1080} width={1920}>
<div className={styles.contentBox}>888</div>
</RScaleScreen>
</div>
.screenBox {
// 让背景色与内容的背景色差不多一致,目的让缩放产生的空白区没有那么明显
position: relative;
width: 100%;
height: 100%;
background-color: #021321;
.contentBox {
width: 1920px;
height: 1080px;
background-color: aliceblue;
}
}
10. 解决项目中两次使用DateV轮播表时,轮播不同步
- datav-react官网:datav-react.jiaminghi.com/guide/
- 示例参考djy_visualization: github.com/Lian-echo/s…
方法一:让高度一致,速度一致
高度差别很小的情况下可以改变高度更高导致更慢的那一边的.row-item的transition: 228ms all;(datav默认transition值为0.3s)
// 轮播表配置项,必须使用状态管理,让数据和配置项统一更新
const [scrollConfig, setScrollConfig] = useState<any>({
header: ["指标项", "指标指数"],
data: [[可以设置默认值]],
waitTime: 3000,
columnWidth: [400, 150],
align: ["left", "center"],
rowNum: 14,
headerHeight: 40,
headerBGC: "#101A23",
evenRowBGC: "rgba(18, 19, 19, 1)",
oddRowBGC: "#101A23",
});
方法二:使用useContext订阅同一个变量(其余共享变量的方法如dva同样适用)
- 考虑两个组件的两个接口响应的时间,一般时间会在三秒之内,先让配置项的waitTime在三秒(即三秒之内不滚动),在接口返回真正的数据之前,展示的都是默认的数据,且不会滚动。
- 等接口返回数据后,替换真实的scrollConfig里的data(此时仍在waitTime的控制时间内,不滚动)
- 两边的接口返回数据后,分别更新context里的变量
if (res?.code === 1) {
setShowEmpty(res?.result.length > 0);
if (res?.result.length == 0) {
return setScrollConfig({
...scrollConfig,
data: targetList, // targetList为默认值
});
}
let newArr: any = [];
setOriginalList(res?.result);
const arr = res?.result.map((item: any, index: number) => {
newArr.push([item.targetName, decimalPoint(item.indicatorIndex)]);
});
// !!!更新context里的变量
setStartNow((prev) => prev + 0.5);
setScrollConfig({
...scrollConfig,
data: newArr, // newArr为真实数据
});
}
4. 两边的组件分别依赖于context的变量,二次更新轮播表配置项,以达到两边同时更新组件的目的
// 两个组件多一次重新初始化,即保证了同时开始滚动
useEffect(() => {
// 双边startNow分别+0.5 即startNow>1时,两边都获取到了真实的数据
if (startNow >= 1 && scrollConfig.data.length) {
console.log("右侧开始滚动", Date.now());
// 重新初始化组件
setScrollConfig({
...scrollConfig,
});
}
}, [startNow, scrollConfig.data]);
5. 即使有哪边的接口报错或者未返回数据,也不影响,等待三秒之后自会滚动,只是不存在二次滚动
11. 打包优化
- 配置analyze命令,执行
npm run analyze
{
...
"scripts":{
...
"analyze": "cross-env ANALYZE=1 umi build",
}
2. 根据打包分析哪些模块占比大
3. 删除无用包,下载npm install -g depcheck,执行depcheck
4. 优化打包方式: 提取公共模块、多核心打包、抽离css等
import { defineConfig } from 'umi';
import AntdDayjsWebpackPlugin from 'antd-dayjs-webpack-plugin';
const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({
size: require('os').cpus().length,
});
export default defineConfig({
define: {
// 'process.env.API_HOST': 'https://41.199.1.112:8080',
// 目前正式环境的API_HOST、PKI_HOST随着部署地址变化,并非固定地址。
'process.env.API_HOST': 'http://xxxx:8088',
'process.env.PKI_HOST': 'https://xxxx:8443',
},
nodeModulesTransform: {
type: 'all',
},
// 将包分为这些模块
chunks: ['react', 'antdpro', 'antd', 'common', 'vendors', 'umi'],
chainWebpack: function (config, { webpack }) {
/** 使用Dayjs替换Antd的Moment,dev下开启mfsu会有错误 */
config.plugin('antd-dayjs-webpack-plugin').use(AntdDayjsWebpackPlugin);
// 加快编译
config.cache({
type: 'filesystem',
allowCollectingMemory: true,
buildDependencies: {
config: [__filename],
},
});
// 利用多线程,加快js打包
config.plugin('HappyPack').use(HappyPack, [{
id: 'js',
loaders: ['babel-loader'],
threadPool: happyThreadPool,
}]);
// 提取公共模块,减小打包体积
config.merge({
optimization: {
splitChunks: {
chunks: 'all', //async异步代码分割 initial同步代码分割 all同步异步分割都开启
automaticNameDelimiter: '.',
minSize: 30000, // 引入的文件大于30kb才进行分割
minChunks: 2, // 模块至少使用次数
cacheGroups: {
react: {
chunks: 'all',
name: 'react',
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
priority: 20,
},
antdpro: {
name: 'antdpro',
chunks: 'all',
test: /[\\/]node_modules[\\/]@ant-design[\\/]pro-.*[\\/]/,
priority: 10,
enforce: true,
},
antd: {
name: 'antd',
chunks: 'all',
test: /[\\/]node_modules[\\/](@ant-design|antd|antd-mobile)[\\/]/,
priority: 9,
enforce: true,
},
// 其他的一些体积比较大的第三方库
common: {
name: 'common',
chunks: 'all',
test: /[\\/]node_modules[\\/](@wangeditor)[\\/]/,
priority: 9,
enforce: true,
},
vendors: {
name: 'vendors',
chunks: 'all',
test: /[\\/]node_modules[\\/]/,
priority: 8,
enforce: true,
},
// 将css打包到一个文件里
styles: {
name: 'styles',
test: /\.(css|less)$/,
chunks: 'async',
minChunks: 1,
minSize: 0,
}
},
},
},
});
},
});
5. 配置压缩器:esbuild、mfsu、terserOptions
{
// esbuild: {}, // 增加压缩速度,可能有bug.如遇bug无法解决。尝试使用terserOptions压缩
terserOptions: {
parse: {
// parse options
},
compress: {
// compress options
},
mangle: {
// mangle options
properties: {
// mangle property options
}
},
format: {
// format options (can also use `output` for backwards compatibility)
},
sourceMap: {
// source map options
},
ecma: 5, // specify one of: 5, 2015, 2016, etc.
enclose: false, // or specify true, or "args:values"
keep_classnames: false,
keep_fnames: false,
ie8: false,
module: false,
nameCache: null, // or specify a name cache object
safari10: false,
toplevel: false
},
}
6. 大体积包按需加载
// 过去
// import { Axis, Chart, Geom, Tooltip, AxisProps } from 'bizcharts';
// 现在
import Chart from 'bizcharts/lib/components/Chart';
import Axis from 'bizcharts/lib/components/Axis';
import Geom from 'bizcharts/lib/components/Geom';
import Tooltip from 'bizcharts/lib/components/Tooltip';
import type AxisProps from 'bizcharts/typings'
7. 第三方工具替换:dayjs替换monment 8. externals加载:设置哪些模块不被打包,通过或其他方式引入
// react、antd、biecharts 这些比较大的包,是否可以使用 externals的方式进行加载?
// 这个要根据业务具体、部署方式、用户的网络情况来判断
export default {
// 配置 external
externals: {
'react': 'window.React',
'react-dom': 'window.ReactDOM',
},
// 引入被 external 库的 scripts
// 区分 development 和 production,使用不同的产物
scripts: process.env.NODE_ENV === 'development' ? [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.development.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.development.js',
] : [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
],
};
12. antd-upload大文件切片上传
注意:前端进行大文件分片上传的方案几乎都是利用Blob.prototype.slice方法对文件进行分片,用数组将每一个分片存起来,最后将分片发给后端。在分片中加入MD5主要是为了后端收到文件后进行校验,要注意的是,Blob对象是不能够作为MD5函数的参数的,一般是用FileReader把Blob读成二进制(arrayBuffer对象)之后再传入MD5函数。对于文件上传的请求,需要用到FormData,http请求头中的Content-Type要设置为multipart/form-data。
- 安装依赖
yarn add spark-md5
yarn add @type/spark-md5
2. 计算文件的 MD5 值
import SparkMD5 from 'spark-md5';
// 计算文件的 MD5 值
const calculateMD5 = (file: any) => {
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
const chunkSize = 2 * 1024 * 1024;
// 文件划分成的分片数量
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
fileReader.onload = function (e: any) {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
const result = spark.end();
resolve(result);
}
};
// 加载下一个分片
function loadNext() {
const start = currentChunk * chunkSize;
const end = Math.min(file?.size, start + chunkSize);
const buffer = file?.slice
? file?.slice(start, end)
: file?.webkitSlice(start, end);
fileReader.readAsArrayBuffer(buffer);
}
loadNext();
});
};
3. 将文件划分成多个分片
const chunkFile = (file: any, chunkSize: number) => {
// 文件划分成的分片数量
const chunks = Math.ceil(file.size / chunkSize);
const chunksList = [];
let currentChunk = 0;
while (currentChunk < chunks) {
const start = currentChunk * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const chunk = file.slice
? file.slice(start, end)
: file.webkitSlice(start, end);
// 将分片添加到列表中
chunksList.push(chunk);
currentChunk++;
}
return chunksList; // 返回分片列表
};
4. upload onChange使用计算文件md5值以及分片函数
const handleChange2 = async ({ file }: any) => {
// console.log(file, 'file');
if (file?.status === 'removed') return;
setVloading(true);
const md5: any = await calculateMD5(file); // 计算文件的 MD5 值
// console.log(md5, 'md5');
md5Ref.current = md5; // 保存 MD5 值到引用
// 将文件分片并保存到引用对象中
const chunksList: any = chunkFile(file, 2 * 1024 * 1024);
console.log(chunksList, 'chunksList');
chunkRefs.current = chunksList?.map((chunk: any, index: number) => {
const formData = new FormData();
formData.append('files', chunk);
formData.append('filename', file.name);
formData.append('total', chunksList.length);
formData.append('index', index.toString());
formData.append('md5', md5Ref.current); // 添加 MD5 参数
return formData;
});
console.log(chunkRefs, 'chunkRefs');
// 定义递归函数用于逐个上传分片
const uploadChunk = async (index: any) => {
if (index >= chunkRefs.current.length) {
// 所有分片上传完成
message.success('文件上传成功!');
setVloading(false); // 文件上传完成,修改上传状态
return;
}
try {
await upload(chunkRefs.current[index]); // 调用上传函数上传当前分片,此处为调用上传的接口
console.log(`分片 ${index + 1} 上传成功`);
// 更新进度条的值
// const newProgress = Math.ceil(
// ((index + 1) / chunkRefs.current.length) * 100,
// );
// setProgress(newProgress);
// 递归调用上传下一个分片
await uploadChunk(index + 1);
return;
} catch (error) {
console.error(`分片 ${index + 1} 上传失败`, error);
message.error('文件上传失败!');
setVloading(false); // 文件上传失败,修改上传状态
return;
}
};
// 开始递归上传第一个分片
await uploadChunk(0);
};
5. upload组件
<Upload
action="/api/openApi/upload"
name="files"
accept="video/*"
beforeUpload={() => false}
onChange={handleChange2}
maxCount={1}
onRemove={handleRemove} // 添加自定义的删除操作
// showUploadList={false}
// fileList={fileList}
>
<Button loading={vLoading} icon={<UploadOutlined />}>
{vLoading ? '上传中' : '开始上传'}
</Button>
</Upload>
13. web页面对接海康摄像头监控
- 拿到摄像头序列号、设备号
- 将设备绑定到萤石里(open.ys7.com/cn/s/14)
- 在VLC播放器测试各个协议推流是否成功
- 项目中使用后端通过接口实现HTTP-FLV协议推流
- 可借用flv.js使用Video展示后端返回的推流地址
import flvjs from "flv.js";
useEffect(() => {
const videoElement: any = document.getElementById("videoElement0");
if (flvjs.isSupported()) {
//判断当前浏览器是否支持播放
const flvPlayer = flvjs.createPlayer({
type: "flv", // 指定视频类型 flv、mp4
isLive: true, // 开启直播
hasAudio: false, //流是否有音频轨道
// hasVideo: false,
url: "https://rtmp01open.ys7.com:9188/v3/openlive/设备号.flv?expire=1730613232&id=id号&t=04aad3e89995acd7f5881e0cfc1918fe33efb6f6f3805ed408f6ab47d46ddb79&ev=100",
// url: "https://upyun.pingangc.com/data/oss/commfile/202310/xxxxxx.flv",
// url: "ws://10.0.1.124:8080/api/video", // 指定流链接 http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4 、ws://10.0.1.124:8080/api/flvstream 。video
});
console.log(flvPlayer, "flvPlayer");
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load(); // 加载数据流
flvPlayer.play(); // 播放数据流
}
}, []);
- 或者ezopen协议推流然后使用iframe嵌套萤石的播放器。
// 动态url: ezopen://open.ys7.com/设备序列号/3.hd.live。监控地址,包含验证码、设备序列号、通道号、清晰度、播放类型
// accessToken:访问令牌,播放监控地址的必要参数。萤石开发者工具-我的设备-获取token
// UIKIT主题名称:输入UIKIT主题模板的ID
<div className={styles.videoBox}>
{itemList?.map((item: any, index: number) => (
<div key={index} className={styles.itemBox}>
<div className={styles.textBox}>
<span className={styles.name} title={item?.deviceName}>
{item?.deviceName}
</span>
{/* <span className={styles.distance}>| {item?.distance}</span> */}
</div>
<div className={styles.videoBox}>
<iframe
src={`https://open.ys7.com/ezopen/h5/iframe?url=${item.deviceUrl}&autoplay=1&accessToken=at.xxxx`}
width="100%"
height="450"
id="ysOpenDevice"
allowFullScreen
></iframe>
{/* <video id={`videoElement${index}`} width={"100%"} height="294" controls poster={parking1} /> */}
</div>
</div>
))}
</div>
- HLS、RTMP、ezopen协议区别
- HTTP-FLV,即将音视频数据封装成FLV,然后通过HTTP协议传输给客户端:
- 除了使用iframe内嵌之外,可以借助ezuikit.js使用Video直接播放ezopen协议的视频流地址
import EZUIKit from 'ezuikit-js';
var player = new EZUIKit.EZUIKitPlayer({
id: 'video-container', // 视频容器ID
accessToken: 'at.3bvmj4ycamlgdwgw1ig1jruma0wpohl6-48zifyb39c-13t5am6-yukyi86mz',
url: 'ezopen://open.ys7.com/203751922/1.live', // 多个监控用逗号分割
})
HLS协议
https: //open.ys7.com/v3/openlive/设备ID_清晰度.m3u8?expire=1722172888&id=607322167286378496&t=5e61826cfe5910a5ecdc4c4b704ba28f2f62d3ea955a7c306885eaa714ae6b89&ev=100
RTMP协议
rtmp://xyrtmp.ys7.com:1935/v3/openlive/设备ID_清晰度?expire=1722172888&id=607322166882480128&t=d8509e6b863f1145d753f8e6f4f94cd2e4b592ec33f0bc78dfcbbbf2d267afc4&ev=100&vc=3&supportH265=1
HTTP-FLV协议
https: //xyrtmp.ys7.com:9188/v3/openlive/设备ID_清晰度.flv?expire=1722172888&id=607322167100575744&t=1f723e53a1d408687c38c20a0d34b1f7d66b4557b6be6f71f64a7d1d96ea19ed&ev=100
参数备注:
该协议表示可以播放ID为“ff01018a141094b7fa138b9d0b856507b”设备“高清”的“RTMP协议实时视频”
expire及后面的参数:用于各个参数的访问权限设置,请勿删除
vc:支持编码,H264跟H265的区别
supportH265:该参数仅用于让设备
EZOPEN协议
ezopen://open.ys7.com/440912260/1.hd.live”,可以播放序列号为“440912260”设备“1通道”“高清”的“预览视频
14. 预览excel(wps其余形式同理)
view.officeapps.live.com/op/view.asp…..
15. PKI证书登录
- 调用唤起识别证书的接口
<a
href={`${PKI_HOST}/login/pki_login?redirectUrl=${
window.location.origin + window.location.pathname
}`}
/>
- 证书识别成功后,会自动刷新页面,此时接口返回一个临时token用来登录
useEffect(() => {
const query = window.location.search;
if (!query) return;
const searchParams = new URLSearchParams(query);
const status = searchParams.get('status');
const token = searchParams.get('token');
if (status !== '10000' || !token)
return notification.warning({ message: '当前证书无效' });
async function getLogin() {
try {
setIsLoading(1);
const data = await loginWithTempToken({ token }); // 用临时token登录,返回的结果才是真实的token
if (!data?.result?.token) {
setIsLoading(2);
return notification.warning({ message: '当前证书无效' });
} else {
successLogin(data); // 登录成功的处理
}
} catch (error) {
setIsLoading(2);
console.log('error ~~~~', error);
}
}
getLogin();
}, []);
- 处理返回成功的结果
const successLogin = (res: any) => {
setStoreSess('userInfo', res?.result);
setStoreSess('token', res?.result?.token);
/** 给官网的iframe调用使用 注:后续PKI登录验证成功也需要下面这块代码*/
if (window.parent !== window) {
window.parent.postMessage(
{ type: 'website-login-message', data: res?.result },
'*', // 最好指定父页面url,暂无
);
setTimeout(() => {
window.close();
}, 100);
}
localStorage.setItem('depCodes', JSON.stringify(res?.result?.depCodes));
history.replace({
pathname: '/news/structure',
});
};
16. 富文本编辑器wangeditor
- 安装
yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save
yarn add @wangeditor/editor-for-react
# 或者 npm install @wangeditor/editor-for-react --save
- 使用
import { Editor, Toolbar } from "@wangeditor/editor-for-react";
<Toolbar
editor={editor}
defaultConfig={toolbarConfig} // 工具栏配置
mode="default"
style={{ borderBottom: "1px solid #ccc" }}
/>
<Editor
defaultConfig={editorConfig} // 编辑器配置
value={html}
onCreated={setEditor}
onChange={(editor) => {
// const temp = editor.getHtml();
// setHtml(temp);
htmlChange(editor.getHtml());
}}
mode="default"
style={{ height: "500px", overflowY: "hidden" }}
/>
- 配置
// 工具栏配置
const toolbarConfig: Partial<IToolbarConfig> = {
excludeKeys: ["group-video"], // 过滤一些菜单中不需要的配置
};
// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
placeholder: "请输入内容...",
MENU_CONF: { // 菜单配置
// 下面这个配置关于所选字体限制的
fontFamily: {
fontFamilyList: [
"黑体",
"楷体",
"仿宋",
"SweiSpringCJKtc-Bold",
"SweiSpringCJKtc-ExtraLight",
"SweiSpringCJKtc-Black",
],
},
uploadImage: {
server: "/api/openApi/upload", // 设置服务器地址
headers: { // 设置http请求头
Authorization: getStoreSess("token"),
},
fieldName: "files",
// 单个文件的最大体积限制,默认为 2M
maxFileSize: 20 * 1024 * 1024, // 20M
// 最多可上传几个文件,默认为 100
maxNumberOfFiles: 10,
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
allowedFileTypes: ["image/*"],
// 上传之前触发
onBeforeUpload(file: File) {
return file;
},
onProgress(progress: number) {
console.log("progress", progress);
},
// 单个文件上传成功之后
onSuccess(file: File, res: any) {
console.log(`${file.name} 上传成功`, res, res.result[0]);
},
// 自定义插入图片
customInsert(res: any, insertFn: InsertFnType) {
insertFn(configSetting.uploadUrl + res.result[0]);
},
// 单个文件上传失败
onFailed(file: File, res: any) {
console.log(`${file.name} 上传失败`, res);
},
},
},
};
- 意外的bug
- 无法预览表格的样式
// 在onChange事件赋值时,重新手动对html追加表格样式
import { tableStyle, cellStyle, headerStyle } from './tableStyle';
export const parseHtml = (editor: any) => {
const $tempContainer = document.createElement('div');
$tempContainer.innerHTML = editor;
$tempContainer.querySelectorAll('img').forEach(($el) => {
if ($el.style.maxWidth !== '100%') {
$el.style.maxWidth = '100%';
$el.style.marginBottom = '10px';
}
});
const tables = $tempContainer.querySelectorAll('table');
tables.forEach(($el) => {
Object.assign($el.style, tableStyle);
const tableCells = $el.querySelectorAll('td, th');
tableCells.forEach(($cell) => {
Object.assign($cell.style, cellStyle);
});
const tableHeaders = $el.querySelectorAll('th');
tableHeaders.forEach(($header) => {
Object.assign($header.style, headerStyle);
});
});
return $tempContainer.innerHTML;
};
<Editor
defaultConfig={editorConfig}
value={html}
onCreated={setEditor}
onChange={(editor) => {
setHtml(editor.getHtml());
const preview: HTMLElement | null =
document.getElementById('preview'); // 预览的容器
if (preview) {
preview!.innerHTML = parseHtml(editor.getHtml());
}
}}
mode="default"
style={{ height: '500px' }}
/>
17. 官网内嵌后台的登录获取用户信息
- 官网内嵌iframe唤起登录
// 签收
sign() {
const loginInfo = window.localStorage.getItem("websiteLogin")
// loginInfo 里记录了一个时间戳,可以判断过了多少时间后,就直接重新登陆。
if (!loginInfo) {
this.iframeDialogVisible = true
this.iframeSrc =
process.env.NODE_ENV === "development"
? process.env.VUE_APP_LOGIN_IFRAME_SRC
: `${window.location.protocol}//${window.location.hostname}:8088/login`
} else {
this.signEvent() // 签收的接口请求
}
},
// iframe
<el-dialog title="登录" :visible.sync="iframeDialogVisible" :width="'900px'">
<iframe :src="iframeSrc" width="100%" height="500" id="loginFrame"></iframe>
</el-dialog>
- 后台登录成功后判断父级窗口是否为windows,不是则向父级窗口发送data
if (window.parent !== window) {
window.parent.postMessage(
{ type: 'website-login-message', data: res?.result },
'*', // 最好指定父页面url,暂无
);
setTimeout(() => {
window.close();
}, 100);
}
- 官网添加事件监听message
mounted() {
window.addEventListener("message", this.messageListener)
},
beforeDestroy() {
window.removeEventListener("message", this.messageListener)
},
// 监听iframe发送的message
messageListener(res) {
const { type, data } = res.data
if (!type || !data) return
if (type !== "website-login-message") return
data.timeStamp = Date.now()
window.localStorage.setItem("websiteLogin", JSON.stringify(data))
this.iframeSrc = "about:blank"
this.iframeDialogVisible = false
},
Echarts:
1. 注册地图
1. 引入地图,初始化地图
import * as echarts from "echarts";
echarts.registerMap('china', {geoJSON: geoJson});
var chart = echarts.init(document.getElementById('main'));
const option = {
layoutCenter: ["50%", "50%"], //位置
layoutSize: "100%", //大小
toolbox: {
// 是否显示工具栏组件
show: true,
},
tooltip: {
// tooltip-提示框组件
position: "right",
show: true,
trigger: "item", // 触发类型:item:'图形触发';axis:'坐标轴触发';none
borderColor: "transparent", // 设置边框线颜色为透明色
borderWidth: 0, // 提示框浮层的边框宽
extraCssText: "box-shadow: none;", // 覆盖默认的边框样式
enterable: false, //鼠标是否可进入提示框浮层中,默认为false,
transitionDuration: 1, //提示框浮层的移动动画过渡时间,单位是 s,设置为 0 的时候会紧跟着鼠标移动
backgroundColor: "transparent",
formatter: function (params: any) {
// params.data-传入的原始数据项
let str = null;
const arr = streedList?.slice(-3).map((item: any) => item.street); // 排名倒数3位
str = `
<div style = " overflow: hidden;background:url( ${
Number(params?.data?.mapNum) < 100 || arr?.includes(params.name)
? require("@/assets/img/mapTipsRed.png")
: require("@/assets/img/mapTipsBlue.png")
}) no-repeat;background-size:contain;padding:14px 3px 13px 15px;font-size: 16px;width:168px;position: absolute;right: 0px;bottom:80px;color:#fff;transform: rotate(-60deg);border-radius: 4px;">
<div style="text-shadow: 1px 2px 2px rgba(1, 51, 76, 0.6);font-weight: 500;">${
params.name
}</div>
<div style="height:50px;display:flex;align-items: center;">
<div style="text-shadow: 1px 2px 2px rgba(1, 51, 76, 0.6);">
<div style="font-size: 14px;margin:10px 0 4px 0;">综合指数:<span style="font-weight: 700;color: #fff;">${
params?.data?.mapNum
}</span></div>
<div style="font-size: 14px;color: #d6e0e7;">${
params?.data?.time
}</div>
</div>
</div>
</div>
`;
return str;
},
},
geo: {
// geo-地理坐标系组件
show: true,
map: "HK",
aspectScale: 1.013, //地图宽高比
zoom: 1.086,
regions: otherArr?.slice(-3).map((item: any) => ({
// regions-在地图中对特定的区域配置样式
name: item.name,
itemStyle: {
// 该区域的多边形样式设置
normal: {
shadowBlur: 5, //图形阴影的模糊大小。
shadowColor: "#000",
shadowOffsetX: 0,
shadowOffsetY: 0,
areaColor: {
//地图色
type: "radial",
x: 0.5,
y: 0.5,
r: 0.9,
colorStops: [
{
offset: 0,
color: "rgba(189, 49, 36, 0.24)", // 0% 处的颜色
},
{
offset: 1,
color: "rgba(172, 76, 68, 1)", // 100% 处的颜色
},
],
global: false, // 缺省为 false
},
},
},
})),
itemStyle: {
// itemStyle-地图区域的多边形 图形样式。
normal: {
areaColor: "transparent",
borderColor: "transparent",
borderWidth: 0,
shadowColor: "rgba(63, 218, 255, 0.5)", // 阴影颜色
shadowBlur: 30, // 阴影模糊大小
},
},
},
series: [
{
type: "map",
name: "都江堰",
map: "HK", // 使用 registerMap注册的地图名称
backgroundColor: "#5EA6D4", // map 背景色
aspectScale: 1.013, // 地图宽高比
zoom: 1.086, // 当前视角的缩放比例
selectedMode: "none", // 设置选中模式为不可选中
itemStyle: {
// 地图区域的多边形 图形样式
normal: {
borderColor: "transparent", //边界线颜色
areaColor: "transparent", // 整个map颜色
shadowBlur: 1, //图形阴影的模糊大小。
// opacity: 1, //图形透明度默认1。支持从 0 到 1 的数字,为 0 时不绘制该图形
},
},
emphasis: {
label: {
color: "#fff",
},
},
label: {
show: true,
color: "#fff", // 字体颜色
rotate: -300, // 这个地方是因为map进行旋转导致字歪斜,使用rotate矫正
},
data: otherArr, // 地图系列中的数据内容数组
},
],
};
chart.setOption(option);
- ECharts 可以使用 GeoJSON 格式的数据作为地图的轮廓
- geojson.io 自行选择想要的区域地图,将生成的json文件导出,注册到ECharts 中
- 注意:若实际展示的地图位置与官方的不一样,可以自行调整地图容器的角度、位置等
2. 调取接口。将展示数据注入地图
const arr = streedList?.slice(-3).map((item: any) => item.street); // 排名倒数3位
const otherArr = streedList?.map((item: any) => ({
mapNum: decimalPoint(item.indicatorItemValue),
time: formatTime(item.effectTime, "days"),
streetId: item.streetId,
name: item.street,
itemStyle: {
// 在data里自定义特殊itemStyle,仅对该item有效。data.itemStyle优先级最高
normal: {
color: "#EEF0F2",
label: {
show: true,
textStyle: {
color: "#fff",
fontSize: 15,
},
},
},
emphasis: {
// 高亮样式
areaColor: {
type: "radial",
x: 0.5,
y: 0.5,
r: 0.8,
colorStops:
Number(item.indicatorItemValue) < 100 || arr?.includes(item.street)
? [
{ offset: 0, color: "rgba(189, 49, 36, 0.24)" },
{ offset: 1, color: "rgba(172, 76, 68, 1)" },
]
: [
{ offset: 0, color: "rgba(8, 83, 114, 0.24)" },
{ offset: 1, color: "rgba(69, 179, 255, 1)" },
],
},
// 设置阴影
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowBlur: 10,
borderWidth: 5,
shadowColor: "rgba(6, 16, 25, 1)",
},
},
}));
2. 雷达图不显示整组数据 只显示当前维度的数据
let dataMax = 100;
const source = {
data: [43, 10, 28, 35, 50, 19, 13],
indicator: [
{ name: '数学', max: dataMax },
{ name: '英语', max: dataMax },
{ name: '语文', max: dataMax },
{ name: '化学', max: dataMax },
{ name: '生物', max: dataMax },
{ name: '物理', max: dataMax },
{ name: '体育', max: dataMax }
]
};
const buildSeries = function (data) {
const helper = data.map((item, index) => {
const arr = new Array(data.length);
arr.splice(index, 1, item);
return arr;
});
console.log([data, ...helper]);
return [data, ...helper].map((item, index) => {
return {
type: 'radar',
tooltip: {
show: index === 0 ? false : true,
formatter: function () {
return (
source.indicator[index - 1].name + source.data[index - 1] + '分'
);
}
},
// z: index === 0 ? 1 : 2,
data: [item]
};
});
};
option = {
tooltip: {},
radar: {
indicator: source.indicator
},
series: buildSeries(source.data)
};
myChart.on('click', (param) => {
console.log(param);
console.log(option.radar.indicator);
console.log(option.radar.indicator[param.event.topTarget.__dimIdx]); // 点击事件可以拿到当前维度值
});
小程序:
1. 为什么没有返回箭头?
可能页面的切换是通过变量展示的,所以始终只有一个页面,而小程序默认首页没有返回箭头 。需要添加一个新的页面,点击跳转到新页面,新页面便会有返回箭头。最后在新页面去链接h5的链接路径,而非在首页。
2. 小程序内嵌网页 web-view
- 小程序设置好src。注意:
navigationStyle: custom对 web-view 组件无效
<WebView
className='webview-container'
// src='http://10.0.1.37:8080/#/map'
src={`${domainName}/#/map`}
onMessage={handleWebViewMessage}
/>
2. 在h5端下载安装weixin-js-sdk
npm install weixin-js-sdk
3. 使用weixin-js-sdk
import wx from 'weixin-js-sdk'
async getJssdk () {
const res = await getTicket({
url: window.location.href.split('#')[0] //向服务端提供授权url参数,并且不需要#后面的部分
})
if (res?.code === 1) {
// 初始化WeChat
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: 'wxxxxxxxxxx', // 必填,企业号的唯一标识,此处填写企业号corpid(小程序id)
timestamp: res.timestamp, // 必填,生成签名的时间戳
nonceStr: res.nonceStr, // 必填,生成签名的随机串
signature: res.signature, // 必填,签名
jsApiList: ['openLocation'] // 必填,需要使用的JS接口列表,所有JS接口列
})
wx.error(function (res) {
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
console.log(88888)
alert('hhhhh')
})
}
},
4. 使用微信内置地图查看位置
handleGo (e) {
// e包含接口返回的地理信息
wx.ready(function () {
wx.checkJsApi({
jsApiList: ['openLocation'],
success: function (res) {
console.log(res, 2)
}
})
wx.openLocation({
latitude: parseFloat(e.latitude), // 纬度,浮点数,范围为90 ~ -90
longitude: parseFloat(e.longitude), // 经度,浮点数,范围为180 ~ -180。
name: e.name, // 位置名
address: e.address, // 地址详情说明
scale: 1, // 地图缩放级别,整型值,范围从1~28。默认为最大
infoUrl: '', // 在查看位置界面底部显示的超链接,可点击跳转
success: function () {
// 设置成功
console.log('成功了')
},
fail: function (res) {
alert('失败了', res, 7)
}
})
})
},
3. 小程序发布后无法调用接口、无法使用上传功能
- 在微信公众平台-开发-开发管理-开发设置-配置对应的服务器域名(必须以https开头)
兼容ie11:
安装 react-app-ployfill、unfetch/polyfill、abortcontroller-polyfill、promise-polyfill
1.为什么要用babel-polyfill
Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而可以在现有环境执行,所以我们可以用ES6编写,而不用考虑环境支持的问题;
有些浏览器版本的发布早于ES6的定稿和发布,因此如果在编程中使用了ES6的新特性,而浏览器没有更新版本,或者新版本中没有对ES6的特性进行兼容,那么浏览器就会无法识别ES6代码,例如IE9根本看不懂代码写的let和const是什么东西?只能选择报错,这就是浏览器对ES6的兼容性问题;
Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。举个例子,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。
项目部署服务器
如何把项目推送到服务器上(内网)
- 准备一个服务器,连接到服务器的工具(纯命令Xshell、可视化Xftp)
- 进入服务器
3. 具体实现服务器跑项目的脚本如下github.com/Lian-echo/s…
const express = require("express");
const path = require("path");
const { createProxyMiddleware } = require("http-proxy-middleware"); // 处理跨域的中间件
const app = express();
const port = 8061; // 设置使用的端口号:不能和在同一服务器的占用相同端口
// 设置代理 http://10.0.5.xxx:8088为后端接口地址
// changeOrigin: true: 是否改变请求头中的origin属性
app.use("/api", createProxyMiddleware({ target: "http://10.0.5.xxx:8088", changeOrigin: true }));
// 设置静态资源的文件夹
app.use(express.static(path.join(__dirname, "dist")));
// 处理所有其他请求,返回index.html文件
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "dist", "index.html"));
});
// 启动服务器
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});
- 补充在本地项目用脚本纯自动化将dist包上传到服务器指定的目录下替换掉dist:github.com/Lian-echo/u…
- 安装
upload-tools - 在bin/release.js中使用upload-tools
const uploadTools = require('upload-tools');
const config = {
host: '10.0.5.xxx', // 服务器地址
username: 'xxxx', // 用户名
password: 'xxx', // 链接服务器的密码
port: '22',
remotePath: '../service/dzzw/xx/xh-xxx', // 需要上传到服务器哪个文件下
};
const commands = ['yarn build:dev'];
uploadTools({ commands, config });
- 在 packeage.json 中配置
"scripts": {
"release": "node ./bin/release.js ",
"build": "cross-env UMI_ENV=prod umi build", // 为了在需要区别在生产环境的时候使用
"build:dev": "cross-env BUILD_ENV=dev umi build", // 为了在需要区别在测试环境的时候使用
}
- 需要判断是否在测试环境下
const isProd = process.env.NODE_ENV === 'production'; // 生产环境
const isBuildDev = process.env.BUILD_ENV === 'dev' // 打包到dev的测试环境
如何把项目推送到luffy上(公网)
使用Docker容器化部署应用程序,在Docker中使用Nginx容器,并增加挂载的功能
- 在项目中配置Dockerfile、nginx.conf文件
- Dockerfile:用于定义Nginx容器的构建规则。在Dockerfile中,我们可以指定基础镜像、安装依赖、拷贝配置文件等操作。
# 使用nginx作为基础镜像
FROM nginx
# ADD nginx.conf /etc/nginx/nginx.conf
# 拷贝dist文件到容器中 /usr/share/nginx/html/:容器里存放前端打包文件的路径
COPY ./dist /usr/share/nginx/html/
- nginx.conf:指定了 Nginx 的监听端口、服务器名称和根目录
worker_processes 1;
events {
worker_connections 1024;
}
http {
# 导入类型配置文件
include mime.types;
# 设定默认类型为二进制流
default_type application/octet-stream;
# 关闭etag、if_modified_since协商缓存函数
etag off;
if_modified_since off;
# 启用sendfile函数
sendfile on;
# 客户端与服务器连接的超时时间为65秒,超过65秒,服务器关闭连接
keepalive_timeout 65;
# 一个server块
server {
# 服务器监听的端口为80
listen 80;
# 服务器名称为website.djyan.com,可以通过website.djyan.com来访问这个server块的服务
server_name website.djyan.com;
# 以root方式设置资源路径
root /usr/share/nginx/html;
# 默认访问的页面,从左依次找到右,直到找到这个文件,然后返回结束请求
index index.html index.htm;
# location中的内容会尝试根据用户请求中的URI来匹配上面的/uri表达式,如果可以匹配,就选择location {}块中的配置来处理用户请求
location / {
add_header Cache-Control 'no-cache, must-revalidate, proxy-revalidate, max-age=0';
try_files $uri /index.html;
}
location ~* \.txt$ {
root /usr/share/nginx/html/resource;
}
}
}
- 安装docker,yarn build打包出一个dist
yarn build
// 或
yarn build:test
3. 构建镜像,推送镜像
docker build -t hub.upyun.com/xxx-dev/pc-xxx:v.1.0.1 .
docker push hub.upyun.com/xxx-dev/pc-xxx:v.1.0.1
4. 容器云手动更新实例
docker:
1.其他一些常用的docker命令(持续汇总)
docker login -u ${username} -p ${pwd} ${DEV_DOCKER_HUB_REGISTRY} // 直接登录
docker login -u ${username} --password-stdin < ./mypwd.txt http://10.0.x.xxx:8888 // 密码放在./mypwd.txt读取
echo ${pwd} | docker login -u ${username} --password-stdin ${DEV_DOCKER_HUB_REGISTRY} // 使用变量
docker images // 查看镜像列表
docker build -t 镜像名称:版本号 . // 构建镜像 (注意 “ . ”不能丢 这个代表Dockerfile在当前目录下 )
eg:docker build -t docker push hub.xxx.com/gongyun-dev/demo:1.1.1 .
docker tag (原)镜像名:版本号 (新)镜像名:版本号 // 格式镜像名称(改名字)
eg:docker tag xxx.com/test:1.20 hub.xxx.com/gongyun-dev/demo:1.1.0
docker push 镜像名称:版本号 // 上传镜像
eg:docker push hub.xxx.com/gongyun-dev/demo:1.1.1
docker rmi 镜像名称:版本号 // 删除镜像
cd / // 到达根目录
ll // 查看目录文件
cd /service // 切换到service目录
cd dzzw // 切换到当前目录下的dzzw文件
cd fe
cd xh-admin
node /xh-admin/service.js // 启动
docker pull 10.0.5.xxx:5000/xh-admin-xxx:v1.0.13 // 拉取镜像
./run.sh xh-admin-xxx v1.0.13 // 运行镜像
chmod +x run.sh //设置文件成可运行程序
docker rmi 10.0.5.xxx:5000/xh-admin-xxx:v1.0.13 // 删除镜像
2.docker登录出现的问题
- 警告:Use --password-stdin
// docker login -u ${username} -p ${pwd} ${DEV_DOCKER_HUB_REGISTRY}
echo ${pwd} | docker login -u ${username} --password-stdin ${DEV_DOCKER_HUB_REGISTRY}
- 错误:Client.Timeout exceeded while awaiting headers):服务器出现问题,响应超时
git:
1.git rebase别的分支失去了当前分支的最新的提交
2.git合并多次提交
git rebase -i HEAD~<number-of-commits>
或
git rebase -i <需要合并的那些提交的最早那次哈希值也可>
- 其中
<number-of-commits>是你想要合并的提交数量。 - 在弹出的编辑器中,将你想要合并的提交的行前面的
pick改为squash或s。保存并关闭编辑器。 - 切记不能将你需要合并的最早的那次提交改为s,必须为pick
3.git更改某次提交的message
-
<number-of-commits>需要包含你需要修改的那次提交 git rebase -i HEAD~ -
在弹出的编辑器中,将你想要修改的提交前面的pick->e
-
git commit --amend -
在编辑器中修改message并保存 推送
4.其他一些常用的git命令(持续汇总)
git cherry-pick xxxx (SHA的值) // 复制提交
git checkout master // 切换master分支
git pull // 拉取master分支
git branch feat-xxxx // 创建分支xxxx
git checkout feat-xxxx // 切换到xxxx分支
git add . // 暂存所有更改
git commit -m 'feat:提交了什么' // 提交所有更改
git push origin feat-xxxx // 将提交的更改推送到远程
git rebase origin/master
git checkout -- * // 丢弃工作区暂未添加到暂存区的所有更改(适用于执行了add之前)
git reset HEAD * // 将暂存区的文件回退到工作区(适用于执行了add之后)
git reset --hard HEAD~2 // 回退2次提交(撤销commit 也撤销add)
--soft ................. // 回退2次提交(撤销commit 不撤销add)
git commit --amend // 进入vim模式 可修改提交的注释
git checkout commitid -b // 本地新的branchName名字 从某一个提交开始创建本地分支
5.推送本地文件夹到远程新仓库
如果是需要在之前仓库提交的基础上推送到新仓库,空项目直接到第三步
- 在某次提交上新建分支,在远程准备好一个新的仓库
- 删除项目里的.git文件
- git init
- git add .
- git commit -m 'init'
- git remote add origin github.com/仓库名字 // 与远程建立连接
- git push --set-upstream origin master
- 切记需要新建.gitignore文件防止把node_modules上传了
6..gitignore模板
react17.x+umi3.5.30的.gitignore模板
# dependencies
/node_modules
/npm-debug.log*
/yarn-error.log
/yarn.lock
/package-lock.json
# production
/dist
*.zip
# misc
.DS_Store
# umi
.umi
.umi-production
.mfsu-production
vue2的.gitignore模板
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
7.gitaction
1、add labeler
1.Create .github/labeler.yml
enhancement:
- head-branch: ["^feat", "feat"]
bug:
- head-branch: ["^fix", "fix"]
core:
- changed-files:
- any-glob-to-any-file: "src/**/*"
github:
- changed-files:
- any-glob-to-any-file: .github/*
docs:
- changed-files:
- any-glob-to-any-file: README.md
docker:
- changed-files:
- any-glob-to-any-file: Dockerfile
dependence:
- changed-files:
- any-glob-to-any-file: package.json
2.Create Workflow:eg. .github/workflows/labeler.yml
name: "Pull Request Labeler"
on:
- pull_request_target // 在 pull_request_target 事件触发时运行
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest // 工作流程运行的环境最新版本的ubuntu(基于linux的操作系统)
steps:
- uses: actions/labeler@v5 // 实现标签自动添加的功能
- 在github远程添加Labels
4. 最终效果
常见的报错:
1、终端错误
1.npm version patch -m 'feat:add version'
- package.json缺少 "version": "1.x.x"
2、js类型错误
1.undefined is not iterabl
- 使用扩展运算符报错,undefined is not iterable (cannot read property Symbol(Symbol.iterator)) at _iterableToArray。...不能作用在undefined上
if (
roleId === 43 ||
[...depCodes, ...(otherCodes || [])]?.includes(uploadDepCode)
)
return true;
return false;