不坠青云之志

941 阅读22分钟

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
  • 元素没有设置宽度时,宽度为内容撑开宽
  1. 相同点:都脱离了标准文档流
  2. 不同点:
  • 浮动脱离了文档流,但是仍然占据内存空间,浮动的框可以向左或向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止,会对其他元素造成影响;
  • 绝对定位是彻底脱离了标准文档流,不占据内存空间,不会对其他元素造成影响

浮动带来的影响: 子元素浮动,父元素的高度会塌陷。解决浮动:

  • 给父级设置高度
  • 父级overfolw:hidden/auto/scroll
  • 父级加浮动
  • 父级加伪元素
  • 同级元素:clear:both

7、实现九宫格

思路:每个盒子都给它一个负边距,边距的距离恰巧就是边框的粗细,这样后面一个盒子就会"叠加"在前面那个盒子的边框上

image.png

/* 子元素 */ 
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、数组操作

  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() 删除第一项元素,返回被删除的元素。设置参数无效
  1. 字符串方法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

  1. 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()

  1. Math.ceil():ceil天花板,向上取整。eg:Math.ceil(11.3)=12
  2. Math.floor():floor地板,向下取整。eg:Math.floor(11.6)=11
  3. Math.round():四舍五入。eg:Math.round(11.5)=12

7、生成随机数

要求:随机生成a,b区间的n个整数,且不能重复

image.png

8、replace

  1. 完全匹配(substr)
let str="Visit Microsoft Microsoft!";
str = str.replace("Microsoft","W3School");
console.log(str); // Visit W3School Microsoft!
  1. 正则式匹配(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. 组件属性的冒号:

  1. 加冒号的,说明后面的是一个变量或者表达式
  2. 没加冒号的后面就是对应的字符串字面量

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配置环境变量

  1. 配置开发环境变量:.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
  1. 配置生产环境变量:.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
  1. 如遇到在不同环境部署时以上生产环境变量不采用。当接口请求路径和当前部署的环境的路径一致时,采用变量:
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做适配

  1. 安装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'
  1. 内容列表:legalityList,搜索关键字:searchVal
  2. 遍历出列表里面所有的title、content分别组成两个数组1.2
  3. 拿到需要高亮的两个dom节点
  4. 在数组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访问某个模块详情

  1. 官网首页后端返回本机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 照片全景查看器

  1. 引入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-100container: 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实现懒加载

  1. 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()

image.png 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
  1. 使用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.音频和字幕同步加载

  1. 借用类似LRC文件编辑器将音频里对应时间轴的字幕提取出来 image.png
  2. 监听播放器的timeupdate事件,获取当前audio.currentTime。遍历歌词数组,在字幕数组中查找与当前时间匹配的字幕
  3. 把匹配的字幕更新。匹配规则:当前遍历的歌词时间轴小于或等于播放器的当前时间,且下一句歌词的时间轴大于播放器的当前时间
// 监听播放器的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里面 1662350162958.jpg
  • 使用回调形式的ref:

(a)把函数体写在ref回调函数里内联函数):把当前节点挂载到当前组件实例。react渲染的时候会自动调用,且调用两次(无影响)。第一次为Null,第二次才有真正的节点。 image.png (b)使用类绑定的回调(与(a)一样) image.png

  • 使用createRef API:createRef调用后可以返回一个容器,该容器存储被ref所标识的节点,无需自己绑定。(推荐使用image.png
  • 函数组件没有实例
  • 尽可能避免使用ref。如此案例中:当事件触发对象为对象本身时,使用even.target就可以获得自身节点。或者利用onChange事件将组件变为受控组件。
  • 何时使用ref:(a)管理焦点,文本选择或媒体播放.(b)触发强制动画.(c)集成第三方 DOM 库

4.react生命周期

  • image.png
  • image.png

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配置环境变量

  1. 配置开发环境变量
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,
    },
  },
  1. 配置生产环境变量
  define: {
    // 目前正式环境的API_HOST、PKI_HOST随着部署地址变化,并非固定地址。
    'process.env.API_HOST': 'http://xxx:8088',
    'process.env.PKI_HOST': 'https://xxx:8443',
  },
  1. 不同环境部署时环境变量
// 请求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问题)

  1. 使用FileReader将上传的PDF文件读取为ArrayBuffer
  2. 创建一个DataView来读取ArrayBuffer中的数据
  3. 并检查PDF文件的魔数是否匹配(PDF文件的魔数是文件开头的四个字节,表示文件的类型。对于PDF文件来说,魔数应为0x25504446(即"%PDF"的十六进制表示))
  4. 如果魔数匹配,则认为文件没有破损;否则,认为文件破损
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. 媒体查询
  2. 百分比布局
  3. 栅格系统
  4. 缩放(优点:简单,代码量少;缺点:1、此方式是按照UI稿等比缩放,当展示的大屏和UI稿很不一样时,会出现周边留白的情况;2、缩放比例过大时,字体图片会出现失真,甚至事件热区会偏移)
scale原理

scale 方案是通过 css 的 transform 的 scale 属性来进行一个 等比例缩放 来实现屏幕适配的,既然如此我们要知道以下几个前提:

  1. 设设计稿的 宽高比 为 a,则在任意显示屏中,只要展示内容的容器的 宽高比 也是 a,则二者为 1:1 只要 等比缩放/放大 就可以做到完美展示并且没有任何白边
  2. 如果设计稿的 宽高比 为 a, 而展示容器 宽高比 不是 a 的时候,则存在两种情况。
    1. 宽高比大于 a,此时宽度过长,计算时基准值采用高度,计算出维持 a 宽高比的宽度。
    2. 宽高比小于 a,此时高度过长,计算时基准值采用宽度,计算出维持 a 宽高比的高度。
  3. 使用的时候将下面函数定时执行,多久执行一次。启动监听窗口变化去执行这个定时器
  4. 可直接下载别的写好的插件:更优化的方案看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})`
  1. 安装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轮播表时,轮播不同步

方法一:让高度一致,速度一致

高度差别很小的情况下可以改变高度更高导致更慢的那一边的.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同样适用)
  1. 考虑两个组件的两个接口响应的时间,一般时间会在三秒之内,先让配置项的waitTime在三秒(即三秒之内不滚动),在接口返回真正的数据之前,展示的都是默认的数据,且不会滚动。
  2. 等接口返回数据后,替换真实的scrollConfig里的data(此时仍在waitTime的控制时间内,不滚动)
  3. 两边的接口返回数据后,分别更新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. 打包优化

  1. 配置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

  1. 安装依赖
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页面对接海康摄像头监控

  1. 拿到摄像头序列号、设备号
  2. 将设备绑定到萤石里(open.ys7.com/cn/s/14)
  3. 在VLC播放器测试各个协议推流是否成功
  4. 项目中使用后端通过接口实现HTTP-FLV协议推流
  5. 可借用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(); // 播放数据流
    }
  }, []);

  1. 或者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协议区别

image.png

  • 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证书登录

  1. 调用唤起识别证书的接口
<a
  href={`${PKI_HOST}/login/pki_login?redirectUrl=${
    window.location.origin + window.location.pathname
  }`}
/>
  1. 证书识别成功后,会自动刷新页面,此时接口返回一个临时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();
  }, []);
  1. 处理返回成功的结果
  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

  1. 安装
yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save

yarn add @wangeditor/editor-for-react
# 或者 npm install @wangeditor/editor-for-react --save
  1. 使用
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" }}
/>
  1. 配置
  // 工具栏配置
  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);
        },
      },
    },
  };
  1. 意外的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. 官网内嵌后台的登录获取用户信息

  1. 官网内嵌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>
  1. 后台登录成功后判断父级窗口是否为windows,不是则向父级窗口发送data
if (window.parent !== window) {
  window.parent.postMessage(
    { type: 'website-login-message', data: res?.result },
    '*', // 最好指定父页面url,暂无
  );
  setTimeout(() => {
    window.close();
  }, 100);
}
  1. 官网添加事件监听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

  1. 小程序设置好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开头)

image.png

兼容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,为当前环境提供一个垫片。

项目部署服务器

如何把项目推送到服务器上(内网)

  1. 准备一个服务器,连接到服务器的工具(纯命令Xshell、可视化Xftp)
  2. 进入服务器

83fa088c845e7e9e43bfbb2961a8ecb.png 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}`);
});
  1. 补充在本地项目用脚本纯自动化将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容器,并增加挂载的功能

  1. 在项目中配置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;
        }
    }
}

  1. 安装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. 容器云手动更新实例

image.png

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登录出现的问题

image.png

  • 警告: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别的分支失去了当前分支的最新的提交

1691479511847.jpg

2.git合并多次提交

git rebase -i HEAD~<number-of-commits>

git rebase -i <需要合并的那些提交的最早那次哈希值也可>
  1. 其中 <number-of-commits> 是你想要合并的提交数量。
  2. 在弹出的编辑器中,将你想要合并的提交的行前面的 pick 改为 squash 或 s。保存并关闭编辑器。
  3. 切记不能将你需要合并的最早的那次提交改为s,必须为pick

3.git更改某次提交的message

  1. <number-of-commits> 需要包含你需要修改的那次提交 git rebase -i HEAD~

  2. 在弹出的编辑器中,将你想要修改的提交前面的pick->e

  3.   git commit --amend
    
  4. 在编辑器中修改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.推送本地文件夹到远程新仓库

如果是需要在之前仓库提交的基础上推送到新仓库,空项目直接到第三步
  1. 在某次提交上新建分支,在远程准备好一个新的仓库
  2. 删除项目里的.git文件
  3. git init
  4. git add .
  5. git commit -m 'init'
  6. git remote add origin github.com/仓库名字 // 与远程建立连接
  7. git push --set-upstream origin master
  8. 切记需要新建.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  // 实现标签自动添加的功能
  1. 在github远程添加Labels

image.png 4. 最终效果

image.png

常见的报错:

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;