大桔子

大桔子 查看完整档案

上海编辑  |  填写毕业院校xx  |  前端 编辑 github.com/hugeorange 编辑
编辑

下一步该干什么呢...

得花点时间想想辙了...

2020年会发生点什么呢?🤔🤔🤔

个人动态

大桔子 赞了文章 · 4月12日

rem, vw, 还是...? 各凭本事的移动端适配方案

前言

2018年最后的法定假期都已经结束了,我相信大部分正在进行或曾经进行过移动端页面开发的同学都或多或少的了解过使用rem进行移动端页面适配的方案以及使用vw的方案,(没了解过的同学可以参见大漠老师的这两篇文章 使用Flexible实现手淘H5页面的终端适配再聊移动端页面的适配)也面临过在不同适配方案间进行抉择的思考,我个人最近对于移动端适配方案也进行了一轮重新的研究,期间,对各种适配方案也有一些自己的见解,正好记录下来与大家一起分享。

vw与rem方案中的一些思考

所以,移动端适配 = vw or rem ?

当然不是。并不是所有场景下的移动端页面都适合采用vw或rem的方案,这类方案的本质是布局等比例的缩放,让页面在不同尺寸的屏幕下有类似于矢量图片缩放的效果,即保证页面各元素之间位置尺寸的比例关系,并让元素可以清晰地展现。
这样的方案更适合于视觉组件种类较多,视觉设计对元素位置的相对关系依赖较强的移动端页面。其实大部分页面都可以使用rem或vw的方案进行适配,但对于文本内容较多,或者说希望引导用户沉浸浏览的页面,我个人并不推荐使用等比缩放的方案,至少并不推荐完全使用等比缩放的方案,对于文本内容还是应该直接使用px这种绝对长度单位,毕竟在大屏手机上用户希望看到的是更多的内容而不是更大的内容。实际上很多这类的网站也确实是直接使用px结合flex等布局方式进行移动端适配的,这个在后面讨论具体技术方案的时候我会举几个例子。

rem方案到底在做什么?

在各种rem适配方案的实现中,有两个核心的点

  • 设置<meta name="viewport" content="xxx">(可以根据dpr缩放viewport,也可以直接使用1倍的视口大小)
  • 根据当前布局的宽度(通常是viewport宽度, 也可以是被限制的最大/最小布局宽度)来设置html元素的font-size

之前我已经提到过,vw和rem的方案的本质都是“等比例缩放”,因为vw和rem都是相对长度单位(relative length unit),可以很好的满足这个需求。区别是vw是viewport width的1%,而rem是html元素的font-size。当我们让html元素的font-size与viewport width产生了关联后,rem就可以模拟出使用vw进行布局的效果了。所以在rem方案中,我们只是在把rem当做是vw的影子。写作rem,读作vw...(剧情似乎狗血起来了... rem: 当然是选择原谅你们啊)
我rem有话说

那直接用vw不就完事儿了吗?

且慢

且慢,当初之所以使用rem的方案流行开来正是因为在那时viewport units的浏览器支持程度不甚理想(IOS 8+, Android 4.4+ 参见viewport units的caniuse)。而相比较之下rem就好多了(IOS 4.1+, Android 2.1+ 参见caniuse),所以对于vw,在当时的大环境下前端想说爱你不容易。

我想这时候有人要说了:“醒醒兄弟 已经8102年了!”
是的,8102年都快要过去了,对于兼容性要求不是特别高的情况下vw也算是可以见天日了,并且也有一些针对vw的补丁方案,但还有一个问题我们要稍微讨论一下...

vw可以完全替代rem吗?

回答依旧是否定的。单纯使用vw进行布局不能限制布局的宽度,对于有这个需求的场景至少还是需要将vw和rem混用来处理边界情况。下文也会更详细地提到这种方案,这里先按下不表。

现有生产环境中移动端适配方案的一点总结

当我们在苦苦地寻找适合自己的道路时,不妨先抬头看看别人是怎么做的。那么现实世界里各家互联网公司的移动端页面都采用了什么样的适配方案呢?有没有一些比较有特色的绝活儿呢?以下我按照页面实际使用的css长度单位作为划分标准,为大家举一些栗子。

px方案

就像开篇提到的,并不是说移动端就一定要使用相对长度单位,传统的响应式布局依然是很好的选择,尤其在新闻,社区等可阅读内容较多的场景直接使用px单位可以营造更好地体验。px方案可以让大屏幕手机展示出更多的内容,更符合人们的阅读习惯。采用这种方案的网站有:

  • 腾讯

    移动端首页主要是新闻内容,需要更好的阅读体验,适合直接使用px进行布局。

  • 知乎

    知乎也是比较典型的追求阅读体验的场景,不出意外的也是直接使用px。

  • 点评

    视觉元素较丰富,依旧采用了px方案,页面基本是flex布局,适配效果很好

  • 头条

    头条的这个方案有点特色,依然会设置html元素的font-size也会加上data-dpr属性并且会对viewport进行scale, 但是最终css的输出还是px,并没有使用rem,并且会对不同dpr下的样式单独定义,如下图所示:

头条的适配方案
这样可以解决1px border的问题,文字大小也不会随屏幕尺寸变动(毕竟文本内容较多),虽然我暂时没找到使用到rem的地方,但确实可以在需要的时候对特殊元素做rem方案的布局,不过这种方案应该会造成css文件大小倍增,而且输出这样的css肯定也少不了构建流程插件的支持,算是一种特定的解决方案吧。

看到这里你以为最终输出px就不能做类似于rem/vw的弹性布局了吗,下面就给大家看一手绝活儿...

  • 淘宝

    什么?给我们看了半天文章结果用的是px?
    其实聪明的你一定很快就会发现在效果上淘宝移动端的适配方案和rem/vw的方案其实是差不多的,元素的样式都是通过js生成的,虽然单位确实是px,但是方案依旧是以375px宽度的尺寸为基准进行缩放的。原理上应该是一种css in js的方案,只不过把rem方案中设置html元素font-size的过程内化到使用js计算元素style的过程中去了。这样的方案涉及到整体的开发框架上的统一与支持,并不算是一个特别通用的方案。好处可能是直接使用px单位结果更为精确。可以说是一手绝活儿了。当然淘宝旗下还是有非常多的产品线的,也未必是同样的适配方案(比如大漠老师文中的例子),这里只针对这个移动端首页来说。

rem方案

rem方案可以说是比较成熟了,出镜率也较高,也就不再赘述了,总的来说rem方案主要分为两种,一种是缩放viewport的方案,如当年的lib-flexible,可以对1px border等细节问题较方便的处理,但会影响到media query。另一种就是不缩放viewport,对1px boder等问题单独引入处理方案。然后对于基准尺寸下的html元素fong-size也有很多不同的定义方式,这个说起来没什么标准可言,我就随便举几个例子说明吧:

不缩放viewport

(以下说明的rem与px的对应关系都是在屏幕宽度为375px的情况下)

缩放viewport

(以下说明的rem与px的对应关系都是在屏幕宽度为375px, viewport scale 0.5的情况下)

vw方案

来了,终于来了!前面说了这么多关于vw的问题,到底有没有现有的产品在大规模的使用vw的方案呢?兼容方案又是怎么做的呢?

  • 京东

    京东的移动端首页采用了vw+rem的布局方式,元素布局上依然使用rem单位,没有缩放viewport, html元素的font-size则使用vw + px fallback的形式,并且使用media query来限制布局宽度,如下图所示

    正常情况下:

京东适配方案 正常情况下

边界情况下

京东适配方案 边界情况下

  • 网易

    网易的方案和京东基本相同,没有缩放viewport,使用media query,只处理html元素的font-size,并限制布局宽度。

  • 饿了么

    饿了么也是采用的vw+rem的方案,不过对viewport进行了缩放,也没有限制布局宽度,html元素的font-size依然由px指定,但是具体元素的布局上使用vw + px fallbak的形式,如下图所示:

饿了么适配方案

可以看到,使用上述两种vw+rem的方案对现有的rem方案的改动都不会很大,都采用了vw + fallback的方式,兼容性问题得到了保证,只是饿了么的方案可能更需要构建过程中的插件支持(关于这个,后面我给你们解释解释什么叫惊喜)。这样来看,对于大漠老师提出的vw方案中使用viewport-units-buggyfill库进行兼容的做法,我个人就并不是很推荐了,因为该库使用了css content属性进行兼容处理,官方文档中就指出了对部分浏览器的img标签有影响 ,需要全局引入一条css规则。且对于需要正常使用content的情况(如:图标字体)也会引起不可避免的冲突,另外也不支持伪元素的兼容。所以从我个人的角度来说,如果你一定要问我使用怎样的vw适配方案,我会推荐给你上述两种vw + rem的方案。

这就是全部了吗?

当然不是,我只是列举了几个比较典型的移动端适配方案,其实在具体实现的细节上可以自行把握的点还是很多的,适合的终归才是最好的,那颗银弹或许不会出现,但我们的手中也从未缺少过利器。

彩(an)蛋(li)部分

相信大多数同学也是有想法在实际开发中把vw融入到现有的移动端适配方案中的。如我上述提到的两种vw+rem方案,只修改html元素font-size的方案对于已经在使用rem方案的同学来说改动的成本并不大,只需要在原本的media query 里(或js生成style时)在font-size: *px后面加上font-size: *vw就可以了,如需限制布局宽度则需多加一点判断。

而对于饿了么那种在使用到长度单位时同时输出rem+vw的方式,肯定是要通过一点额外的插件来做了。如果你和我一样刚好在使用Stylus作为css预处理器,那我专门写了一个Stylus的插件用来帮你处理这个问题。
这个插件可以让你在开发流程使用px书写css, 和现有的部分插件不同的是,它同时支持多种适配方案的输出,目前支持rem,纯vw方案以及刚才提到的vw+rem方案的输出。并且对不希望转换px的场景做了很方便的处理。也就是说,如果你现在使用rem方案,可以直接替换使用该插件,当你需要切换到vw或vw+rem方案时基本可以做到无缝切换。

具体的使用方式和示例请参见pandaGao/stylus-px-to-relative-unit

懂我的意思吧

查看原文

赞 43 收藏 26 评论 4

大桔子 发布了文章 · 3月21日

React ssr框架Next.js 的生产实践

首先感慨一句,next 真是一个版本狂人啊,一周就一个下版本

1. 按照官网的方式初始化一个Next.js项目遇到的一些问题

  • 默认初始化的框架样式支持sass,组件级别的[name].module.css,但默认不支持less,官方提供了配置less的方法

    `yarn add @zeit/next-less less`
    // next.config.js
    const withLess = require('@zeit/next-less')
    module.exports = withLess({
      webpack(config, options) {
          config.resolve = {
              alias: {
                ...config.resolve.alias,
                // 配置alias同时要在tsconfig.json 配置 baseUrl,paths
                "@": path.resolve(__dirname, ".") 
              },
              extensions: [".ts", ".tsx", ".js", ".jsx", ".less", ".css"]
        };
        return config
      }
    })

    重新启动后会报 Error: Cannot find module 'webpack' 于是就安装webpack但默认安装的是webpack@5.x,按照这个 issue,要替换成 webpack@4.x 才可以

  • Next.js 打包后静态资源路径中的 _next 目录哪来的 ?
    参考此文
    打包配置 配置assetPrefix,相当于webpack里面的publicPath(将静态资源放在cdn)
    但打包后html引入的静态资源地址会多一层_next目录,解决方案:
    自己代码里创建 _next 目录太过繁琐,且消耗性能,故让运维在拷贝静态资源时,在oss目录上多加一层 _next 目录
  • ssr框架 ajax 请求库的选择node-fetchwindow, document的使用注意事项
  • getInitialProps getStaticProps getStaticPaths getServerSideProps区别
  • clint Link 跳转时子页面样式文件获取不到(好像只有开发阶段有次问题,暂时未解决)
  • 当进行重构项目时,如果不想改变老项目的url(可能由于url已经被百度seo收录),可以利用自定义node服务器,将老url转发到新的url,参考文档但是有个问题这种被转发的url为什么浏览器请求会是 404,但页面还是会正常显示,不知道是为什么???

2. 生产部署

  • 接入公司cicd规范,部署没有使用传统的pm2,而是把服务放在一个容器内,服务重启靠node提供一个健康检查接口,容器来保证服务稳定,当该接口没有正常返回docker容器就会重启应用
  • 由于公司规范启动node服务都是在容器内用 node app.js,但next默认启动是靠next start启动生产服务的。解决方案是:next提供了自定义服务器,我这里采用了koa,将next作为一个中间件来处理参考

    // https://www.nextjs.cn/docs/advanced-features/custom-server
    const Koa = require('koa') // 引入 koa
    const next = require('next') // nextjs 作为中间件
    const routesMap = require('./route') // 新老url冲突时,将老url转发
    
    const dev = process.env.NODE_ENV === 'development' // 判断是否处于开发者状态
    const app = next({ dev }) // 初始化 nextjs,判断它是否处于 dev:开发者状态,还是production: 正式服务状态
    const handle = app.getRequestHandler() // 拿到 http 请求的响应
    
    console.log('next dev环境===>', dev)
    
    const port = 8080
    
    // app.prepare:编译 pages 文件夹下面的页面文件,then 是保证 pages 下页面全部编译完了之后,我们才能真正的启动服务来响应请求。
    // 如果这些内容我们没有编译完成,那么启动服务响应请求的时候可能会报错。
    app.prepare().then(() => {
      const server = new Koa() // 声明一个 server
    
      /** 这是 Koa 的核心用法:中间件。通常给 use 里面写一个函数,这个函数就是中间件。
       * params:
       *  ctx: Koa Context 将 node 的 request 和 response 对象封装到单个对象中,为请求上下文对象
       *  next: 调用后将执行流程转入下一个中间件,如果当前中间件中没有调用 next,整个中间件的执行流程则会在这里终止,后续中间件不会得到执行
       */
      server.use(async (ctx, next) => {
        if (routesMap[ctx.path]) {
          // 老url转发~~~~,这种操作页面可以正常出来,但不知道为什么浏览器状态码会是404
          await app.render(ctx.req, ctx.res, routesMap[ctx.path])
        } else {
          await handle(ctx.req, ctx.res)
        }
        ctx.response = false
      })
    
      server.listen(port)
      console.log(`请访问: http://localhost:${port}`)
查看原文

赞 0 收藏 0 评论 0

大桔子 发布了文章 · 1月29日

Browserslist: caniuse-lite is outdated

Browserslist: caniuse-lite is outdated. Please run the following command: yarn upgrade

image.png

image.png

  • 更新记录如下:
  1. yarn upgrade caniuse-lite browserslist 没用依旧报上面的错
  2. 删除node_modules 重装 == 依旧没用
  3. 删除 yarn.lock == yarn install 不报上述错误了,项目build却炸了,不能随意删除lock文件啊
  4. npx browserslist@latest --update-db参考
  5. 上述操作之后,不报 Browserslist ,报 postcss autoprefixer 错误
  6. 去掉webpack.config.js 里面 postcss-loader 变得正常了
  7. 遂更新 postcss-loader, autoprefixer 至最新,依旧报错,参考stackoverflow解决了

另计:换了一台电脑,执行到第四步就好了,真是难以捉摸的npm

查看原文

赞 0 收藏 0 评论 0

大桔子 赞了回答 · 2020-11-09

js如何通过a标签将下载的文件保存到当前js文件的相对路径?

??? 下载是下载到客户端吧…… 保存到当前js相对路径是服务器端吧???

关注 4 回答 4

大桔子 发布了文章 · 2020-11-06

h5直播拉流页面调研

移动端:https://cloud.tencent.com/developer/ask/24304

pc端:hls.js 支持m3u8

视频编码格式

  • 视频文件格式(容器格式)
  • 视频编解码器(视频编码格式)
  • 视频一开始由两个端采集,视频输入口、音频输入口。采集的数据会分别进行相关处理,简而言之就是:将视频/音频流通过一定的手段转换为比特流并且压缩,最终,这里将比特流以一定顺序放到一个盒子里进行存放,从而声称我们最终所看到的音视频格式。如:mp3/mp4/flv

常用直播协议

协议原理传输协议播放器延时兼容
RTMP每个时刻的数据收到后立即转发TCPflash1-3s需要flash播放器,借助video.js 实现浏览器端播放(pc端)
HLSHTTP Live Streaming ,集合一段时间的数据m3u8,生成ts切片文件,播放完一个列表,在更新下一个列表HTTPvideo10-30s苹果公司实现 IOS 和 高版本 Android均支持, h5可以直接播放
HTTP-FLVFLV 是专门针对 Flash 播放器的HTTP1-3sh5支持(需要支持MSE的浏览器)mse兼容情况
RTMP 全称 Real Time Messageing Protocol 即时消息传送协议
  • Adobe 公司为flash播放器和服务器之间音视频传输开发的私有协议,工作在TCP之上的明文协议

优点:

  RTMP是转为流媒体开发的协议,对底层优化比其他协议更加优秀,同时他Adobe flash支持好,基本上所有的编码器(摄像头之类)都支持RTMP输出

  RTMP由TCP长连接协议,所以客户端向服务端推流这些操作而言,延时性很低。

  pc 主要是 windows ,windows的浏览器基本都支持flash,另外 RTMP适合长时间播放,最后RTMP的延迟相对较低,一般延时1-3s

缺点:

  基于TCP传输,非公共端口,可能会被防火墙拦截

  另一方面,RTMP为 Adobe私有协议,很多设备无法播放,特别是在 ios 端,需要第三方解码器才能播放

  无法在ios的H5页面播放,但ios原生应用可以自己写解码去解析
FLV (Flash Video) 是Adobe的另一种视频格式,是一种在网络上的流媒体数据存储容器格式

优点:

  格式相对简单轻量,不需要很大媒体头部信息

  flv 由 The FLV Header,The Flv Body以及其Tag组成。因此加载速度很快。采用FLV格式封装的文件后缀为 .flv

  我们所说的HTTP-FLV即流媒体数据封装成FLV格式,然后通过HTTP协议传输给客户端

  HTTP-FLV 依靠MIME的特性,根据协议中的Content-Type来选择相应的程序去处理相应的内容,使得流媒体可以通过HTTP传输

  相较于 RTMP协议,HTTP-FLV 能够好的穿透防火墙,它是基于HTTP/80传输
  flv.js
   愿景:从flash视频时代完整的过度到h5时代

   原理:用js解析FLV格式的音视频数据,再通过 Media Source Extensions API 喂给原生HTML5标签(H5原生仅支持播放mp4、webm、ogg等,不支持flv)

   兼容情况 [https://www.jianshu.com/p/c102ae2a319d](https://www.jianshu.com/p/c102ae2a319d)

   参考:用flv.js做直播 [https://github.com/gwuhaolin/blog/issues/3](https://github.com/gwuhaolin/blog/issues/3)

   原理:[https://www.zhihu.com/question/51997376](https://www.zhihu.com/question/51997376)

   Media Source Extensions

    在没有MSE出现之前,前端对 video 的操作,仅仅局限在对视频文件的操作,而并不能对视频流做任何相关操作

    现在MSE提供一些列接口,是开发者可以直接提供 media stream

    使用flv.js 快速搭建html5网页直播:[https://zhuanlan.zhihu.com/p/94440420](https://zhuanlan.zhihu.com/p/94440420)
HLS  
  它不是一下请求完整流,由.m3u8文件和 .ts播放文件组成,服务器会将接受到的视频流进行缓存,

  然后缓存到一定程度后,会将这些视频流编码格式化同时会生成一份 .m3u8 文件和其他很多的.ts文件

  客户端只要不停地按序播放从服务器获取到的文件,从而实现播放音视频

  .m3u8 文件只是存放了一些ts文件的配置信息和相关路径,当视频播放时 .m3u8 是动态改变的,video 标签会解析这个文件

  并找到对应的ts文件来播放,一般为了加快速度 .m3u8 放在web服务器上,ts放在cdn上
m3u8 文件信息
#EXTM3U                            # m3u文件头
#EXT-X-VERSION:3 
#EXT-X-MEDIA-SEQUENCE:2133         # 第一个TS分片的序列号
#EXT-X-TARGETDURATION:6            # 每个分片TS的最大时长
#EXTINF:6.375,                     # 指定每个媒体段(ts)的持续时间,仅对后面的 URI 有效
huputv-ali-live.arenacdn.com_SFlDeHJTSU51NTdV_pbk550-1587696415663.ts?auth_key=1587706605-0-0-10fedd520ec472c1a5dc39d0dcf2984f
#EXTINF:6.375,
huputv-ali-live.arenacdn.com_SFlDeHJTSU51NTdV_pbk550-1587696421958.ts?auth_key=1587706605-0-0-c8d8918ccba170a99f518b55aa2ea290
#EXTINF:6.375,
huputv-ali-live.arenacdn.com_SFlDeHJTSU51NTdV_pbk550-1587696428405.ts?auth_key=1587706605-0-0-f00ab35ac2c77091ac1e7ef693cde112
#EXTINF:6.375,
huputv-ali-live.arenacdn.com_SFlDeHJTSU51NTdV_pbk550-1587696434822.ts?auth_key=1587706605-0-0-08051ceb940b5d0a52292ab9c6e510dd
#EXTINF:6.375,
huputv-ali-live.arenacdn.com_SFlDeHJTSU51NTdV_pbk550-1587696441198.ts?auth_key=1587706605-0-0-fba4c6b281980017fa08a6139fc74a9d
#EXTINF:6.375,
huputv-ali-live.arenacdn.com_SFlDeHJTSU51NTdV_pbk550-1587696447480.ts?auth_key=1587706605-0-0-c3b335be3abe37b37af3339875610851
  HLS协议的请求流程
  1. HTTP请求 m3u8的url(video 标签)
  2. 服务端返回一个 m3u8的播放列表,这个播放列表(TS)是实时更新的
  3. 客户端解析m3u8 的播放列表,再按序请求每一段url获取ts流
编码:以H.264格式对图像进行编码,以mp3或者HE-AAC对声音进行编码,最终打包到MPEG-2 TS(Transport Stream)容器之中
分割:把编码好的TS文件等长切分成后缀为ts的小文件,并生成 .m3u8 的纯文本索引
使用方便:客户端使用一个URL区下载m3u8文件(索引文件确保ts文件顺序),然后开始下载ts文件,下载完成后即开始使用播放器播放
  • 优点:
苹果公司开发的协议,ios全系列产品原生支持,不需要任何插件,Android 也支持

穿透防火墙,基于HTTP/80传输,有效避免防火墙拦截

性能高。通过 HTTP传输,CDN支持良好,且自带码率自适应(Apple 在提出HLS时,就已经考虑了码率自适应问题)
  • 缺点:
实时性差,延迟高,HLS的延迟基本在10s+以上

1. TCP握手

2. m3u8 文件下载

3. m3u8 下的ts文件下载,每个ts文件大概存放5-10s的时长,并且每个 m3u8 文件会存放 3-8个ts文件
如果把ts文件时长足够小,m3u8存放文件足够少,则理论延迟会降低,但会增加服务器压力

同时直播会受网络波动影响较大

文件碎片,特性的双刃剑,ts切片较小,会造成海量文件,对存储和缓存都有一定的挑战
  • 使用方便:
<video controls autoplay>  
    <source data-original="http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8" type="application/vnd.apple.mpegurl" /> 
    <p class="warning">Your browser does not support HTML5 video.</p>  
</video>
市面主流h5直播平台

image
image

声网:webrtc 直播sdk兼容情况:

https://docs.agora.io/cn/Interactive%20Broadcast/start_live_web?platform=Web

利用 JSMpeg + socket 进行直播

由于video移动端糟糕的体验,JSMpeg 另辟蹊径,利用 canvas 渲染视频帧播放视频 audio 播放音频

该方案 可以自由布局

https://github.com/phoboslab/jsmpeg

https://tantao.me/2017/12/23/Canvas%E5%AE%9E%E7%8E%B0%E7%A7%BB%E5%8A%A8%E7%AB%AF%E7%BD%91%E9%A1%B5%E8%A7%86%E9%A2%91%E7%9B%B4%E6%92%AD/

直播整体流程

视频采集端:可以是电脑上音视频输入设备、手机端摄像头、麦克风(RTMP流传输视频数据)

直播流视频服务端:一台nginx服务器,采集视频录制端传输的视频流(H264/ACC编码),由服务端进行解析编码,推送RTMP/HLS格式视频流至视频播放端

视频播放端:可以是电脑上的播放器(QuickTime Player VLC),手机端的native播放器,还有就是H5的 video标签

image.png

  • h5录制视频,使用webRTC(web Real-Time Communication)是一个支持网页浏览器进行实时语音或视频对话的结束
  • 缺点只在pc的Chrome上支持较好,移动端支持不理想
    1.  调用 window.navigator.webkitGetUserMedia({video: true, audio: true}, res => console.log(res), err=>console.log(err)) 获取pc摄像头视频数据
    2.  将获取到的视频数据转换成 window.webkitRTCPeerConnection 一种视频数据流格式
    3.  利用 websocket 将视频数据流数据传输到服务端
    4. webrtc采集视频流
    <!DOCTYPE html>
        <html>
        <head>
            <title>rtc</title>
        </head>
        <body>
            <video id="webcam"></video>
            <img data-original="">
            <canvas style="display:none;"></canvas>
        </body>
        <script type="text/javascript">
            var constraints = {video: true, audio: true};


            // function getSnap() {
            //     var video = document.getElementById('webcam');
            //     var canvas = document.querySelector('canvas');
            //       var ctx = canvas.getContext('2d');
            //       var localMediaStream = null;

            //       function snapshot() {
            //         if (localMediaStream) {
            //           ctx.drawImage(video, 0, 0);
            //           // “image/webp”对Chrome有效,
            //           // 其他浏览器自动降为image/png
            //           document.querySelector('img').src = canvas.toDataURL('image/webp');
            //         }
            //     }

            //     video.addEventListener('click', snapshot, false);

            //     navigator.getUserMedia({video: true}, function(stream) {
            //         // video.src = window.URL.createObjectURL(stream);
            //         video.srcObject = stream
            //         video.play();
            //         localMediaStream = stream;
            //     }, err => console.log(err));

            // }

            // getSnap()



            function getVideo() {
                function onSuccess(stream) {
                    var video = document.getElementById('webcam');
                    video.srcObject = stream;
                    // 前处理编码(如:美颜、滤镜、AR特效等,可能需要借助webassembly处理 )
                    // RTMP 推流
                    video.play();
                }

                function onError(error) {
                  console.log("navigator.getUserMedia error: ", error);
                }

                navigator.getUserMedia(constraints, onSuccess, onError);
            }

            // getVideo();
        </script>
        </html>
IOS原生应用调用摄像头录制过程

搭建Nginx+RTMP直播流服务
  1. 安装 nginx-rtmp-module 或者 simple-nginx-rtmp-module
  2. nginx.conf 配置 RTMP/HLS
  3. 重启nginx服务
服务端流转换格式、编码推流

服务端接收到采集视频录制端传输过来的视频流时,需要对其进行编码,推送 RTMP/HLS/FLV格式视频流至视频播放端

常用编码库方案:x264编码 faac编码 ffmpeg 编码

参考文章:https://www.bilibili.com/read/cv193334/

西瓜视频播放器调研

小程序直播 

<live-pusher mode="HD" url="rtmp://">
负责对手机摄像头和麦克风的数据进行采集和编码,并通过url参数指定的rtmp推流地址上传到云端

<live-player mode="live"> 
小程序内部的一个在线播放器,负责从云端实时拉去音视频数据进行解码和渲染,http-flv 协议播放

mode="RTC"

RTC模式:实现端到端能够以很低的时延传输音视频数据,用于实时视频通话

官方文档:https://developers.weixin.qq....
https://developers.weixin.qq....

教程:
https://blog.51cto.com/phperv...
https://zhuanlan.zhihu.com/p/...

主流播放器介绍:
xgplayer、alipayer 、ckplayer、clappr

原理:播放 m3u8 或者 flv 格式的直播流(移动端天生支持m3u8)底层都是依赖 hls.js 或者 flv.js 将其转码喂给 video 标签

移动端直接使用 video 标签即可

主流播放器:https://github.com/Tinywan/ht...

参考文章

参看文章:

全面进阶h5直播 https://zhuanlan.zhihu.com/p/...
腾讯Bugly-h5视频直播哪些事 https://zhuanlan.zhihu.com/p/... 推荐 hls 流
h5直播起航 https://aotu.io/notes/2016/10...
H5直播 MSE(Mdia Source Extensions)https://www.jianshu.com/p/1bf...
企鹅直播点播实践 https://zhuanlan.zhihu.com/p/... 优先: webrtc、降级:hls
直播协议选择RTMP vs HLS http://www.samirchen.com/ios-...
直播问题踩坑 https://www.cnblogs.com/1wen/...

pc 端 利用 hls.js 播放 m3u8 https://my.oschina.net/yizhic...https://www.zhihu.com/questio...

hls.js 原理:https://juejin.im/entry/5a02d...
花椒直播:flv.js hls.js 分析 https://zhuanlan.zhihu.com/p/...

查看原文

赞 2 收藏 1 评论 0

大桔子 赞了回答 · 2020-09-09

chrome 57版本network面板中,没有了filter选项

那个红的沙漏就是filter啊

关注 3 回答 2

大桔子 收藏了文章 · 2020-08-24

package.json 非官方字段集合

package.json 非官方字段集合

package.json 官方字段请参考 https://docs.npmjs.com/files/package.json。下面介绍的是非官方字段,也就是各种工具定义的相关字段。

1. yarn 相关字段

yarn: 类似 npm 的依赖管理工具,但 yarn 缓存了每个下载过的包,所以再次使用时无需重复下载,同时利用并行下载以最大化资源利用率,因此安装速度更快。

flat

{
  "flat": true
}

如果你的包只允许给定依赖的一个版本,你想强制和命令行上 yarn install --flat 相同的行为,把这个值设为 true

详细参考 yarn - flat.

resolutions

{
  "resolutions": {
    "transitive-package-1": "0.0.29",
    "transitive-package-2": "file:./local-forks/transitive-package-2",
    "dependencies-package-1/transitive-package-3": "^2.1.1"
  }
}

允许你覆盖特定嵌套依赖项的版本。有关完整规范,请参见选择性版本解析 RFC

详细参考 yarn - resolutions.

2. unpkg 相关字段

unpkg: 让 npm 上所有的文件都开启 cdn 服务。

unpkg

# jquery
{
  "unpkg": "dist/jquery.js"
}

正常情况下,访问 jquery 的发布文件通过 https://unpkg.com/jquery@3.3.1/dist/jquery.js,当你使用省略的 url https://unpkg.com/jquery 时,便会按照如下的方式获取文件:

# [latestVersion] 指最新版本号,pkg 指 package.json

# 定义了 unpkg 属性时
https://unpkg.com/jquery@[latestVersion]/[pkg.unpkg]

# 未定义 unpkg 属性时,将回退到 main 属性
https://unpkg.com/jquery@[latestVersion]/[pkg.main] 

详细参考 https://unpkg.com.

3. TypeScript 相关字段

TypeScript: JavaScript 的超集

types, typings

{
  "main": "./lib/main.js",
  "types": "./lib/main.d.ts"
}

就像 main 字段一样,定义一个针对 TypeScript 的入口文件。

详细参考 TypeScript documentation.

4. browserslist 相关字段

browserslist: 设置项目的浏览器兼容情况。

browserslist

{
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ]
}

支持的工具:

详细参考 browserslist.

5. 发行打包相关字段

点击 Setting up multi-platform npm packages 查看相关介绍。

module

{
  "main": "./lib/main.js",
  "module": "./lib/main.m.js"
}

就像 main 字段一样,定义一个针对 es6 模块及语法的入口文件。

构建工具在构建项目的时候,如果发现了这个字段,会首先使用这个字段指向的文件,如果未定义,则回退到 main 字段指向的文件。

支持的工具:

详细参考 rollup - pkg.module.

browser

{
  "main": "./lib/main.js",
  "browser": "./lib/main.b.js"
}

指定该模块供浏览器使用的入口文件。

如果这个字段未定义,则回退到 main 字段指向的文件。

支持的工具:

详细参考 babel-plugin-module-resolver.

esnext

{
  "main": "main.js",
  "esnext": "main-esnext.js"
}

# or

{
  "main": "main.js",
  "esnext": {
    "main": "main-esnext.js",
    "browser": "browser-specific-main-esnext.js"
  }
}

使用 es 模块化规范,stage 4 特性的源代码。

详细参考 Transpiling dependencies with Babel, Delivering untranspiled source code via npm.

es2015

{
  "main": "main.js",
  "es2015": "main-es2015.js"
}

Angular 定义的未转码的 es6 源码。

详细参考 https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/edit#.

esm

详细参考 adjusted proposal: ES module "esm": true package.json flag.

6. react-native 相关字段

react-native: 使用 react 组件技术写原生APP。

react-native

{
  "main": "./lib/main.js",
  "react-native": "./lib/main.react-native.js"
}

指定该模块供 react-native 使用的入口文件。

如果这个字段未定义,则回退到 main 字段指向的文件。

源代码查看.

7. webpack 相关字段

sideEffects

{
  "sideEffects": true|false
}

声明该模块是否包含 sideEffects(副作用),从而可以为 tree-shaking 提供更大的优化空间。

详细参考 sideEffects example, proposal for marking functions as pure, eslint-plugin-tree-shaking.

8. microbundle 相关字段

microbundle: 基于 rollup 零配置快速打包工具。

source

{
  "source": "src/index.js"
}

源文件入口文件。

详细参考 Specifying builds in package.json.

umd:main

{
  "umd:main": "dist/main.umd.js"
}

umd 模式 bundle 文件。

详细参考 Specifying builds in package.json.

8. parcel 相关字段

parcel: 零配置打包工具。

source

查看 parcel-bundler/parcel#1652.

9. babel 相关字段

babel: es6 -> es5 转码器。

babel

配置 babel

10. eslint 相关字段

eslint: js 代码检查与优化。

eslintConfig

配置 eslint

11. jest 相关字段

jest: js 测试库。

jest

{
  "jest": {
    "verbose": true
  }
}

配置 jest

详细参考 jest docs.

12. stylelint 相关字段

stylelint: style 代码检查与优化。

stylelint

配置 stylelint

详细参考 New configuration loader.

13. ava 相关字段

ava: js 测试库。

ava

{
  "ava": {
    "require": [ "@std/esm" ]
  }
}

配置 ava

详细参考 ava configuration.

14. nyc 相关字段

nyc: istanbul.js 命令行。

nyc

{
  "nyc": {
    "extension": [".js", ".mjs"],
    "require": ["@std/esm"]
  }
}

配置 nyc

详细参考 nyc docs.

15. CommonJS 保留字段

保留字段: build, default, email, external, files, imports, maintainer, paths, platform, require, summary, test, using, downloads, uid.

不可用字段: id, type, 以 _$ 开头的字段。

16. Standard JS 相关字段

Standard JS: js 代码检查与优化。

standard

{
  "standard": {
    "parser": "babel-eslint",
    "ignore": [
      "**/out/",
      "/lib/select2/",
      "/lib/ckeditor/",
      "tmp.js"
    ]
  }
}

配置 standard.

详细参考 https://standardjs.com/.

17. 其他

style

声明当前模块包含 style 部分,并指定入口文件。

支持的工具:

详细参考 Package.json "style" Attribute, istf-spec.

less

style 一样,但是是 less 文件。

支持的工具:

18. 更多

参考 package.json fields explained.

19. 后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

查看原文

大桔子 赞了文章 · 2020-08-24

package.json 非官方字段集合

package.json 非官方字段集合

package.json 官方字段请参考 https://docs.npmjs.com/files/package.json。下面介绍的是非官方字段,也就是各种工具定义的相关字段。

1. yarn 相关字段

yarn: 类似 npm 的依赖管理工具,但 yarn 缓存了每个下载过的包,所以再次使用时无需重复下载,同时利用并行下载以最大化资源利用率,因此安装速度更快。

flat

{
  "flat": true
}

如果你的包只允许给定依赖的一个版本,你想强制和命令行上 yarn install --flat 相同的行为,把这个值设为 true

详细参考 yarn - flat.

resolutions

{
  "resolutions": {
    "transitive-package-1": "0.0.29",
    "transitive-package-2": "file:./local-forks/transitive-package-2",
    "dependencies-package-1/transitive-package-3": "^2.1.1"
  }
}

允许你覆盖特定嵌套依赖项的版本。有关完整规范,请参见选择性版本解析 RFC

详细参考 yarn - resolutions.

2. unpkg 相关字段

unpkg: 让 npm 上所有的文件都开启 cdn 服务。

unpkg

# jquery
{
  "unpkg": "dist/jquery.js"
}

正常情况下,访问 jquery 的发布文件通过 https://unpkg.com/jquery@3.3.1/dist/jquery.js,当你使用省略的 url https://unpkg.com/jquery 时,便会按照如下的方式获取文件:

# [latestVersion] 指最新版本号,pkg 指 package.json

# 定义了 unpkg 属性时
https://unpkg.com/jquery@[latestVersion]/[pkg.unpkg]

# 未定义 unpkg 属性时,将回退到 main 属性
https://unpkg.com/jquery@[latestVersion]/[pkg.main] 

详细参考 https://unpkg.com.

3. TypeScript 相关字段

TypeScript: JavaScript 的超集

types, typings

{
  "main": "./lib/main.js",
  "types": "./lib/main.d.ts"
}

就像 main 字段一样,定义一个针对 TypeScript 的入口文件。

详细参考 TypeScript documentation.

4. browserslist 相关字段

browserslist: 设置项目的浏览器兼容情况。

browserslist

{
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ]
}

支持的工具:

详细参考 browserslist.

5. 发行打包相关字段

点击 Setting up multi-platform npm packages 查看相关介绍。

module

{
  "main": "./lib/main.js",
  "module": "./lib/main.m.js"
}

就像 main 字段一样,定义一个针对 es6 模块及语法的入口文件。

构建工具在构建项目的时候,如果发现了这个字段,会首先使用这个字段指向的文件,如果未定义,则回退到 main 字段指向的文件。

支持的工具:

详细参考 rollup - pkg.module.

browser

{
  "main": "./lib/main.js",
  "browser": "./lib/main.b.js"
}

指定该模块供浏览器使用的入口文件。

如果这个字段未定义,则回退到 main 字段指向的文件。

支持的工具:

详细参考 babel-plugin-module-resolver.

esnext

{
  "main": "main.js",
  "esnext": "main-esnext.js"
}

# or

{
  "main": "main.js",
  "esnext": {
    "main": "main-esnext.js",
    "browser": "browser-specific-main-esnext.js"
  }
}

使用 es 模块化规范,stage 4 特性的源代码。

详细参考 Transpiling dependencies with Babel, Delivering untranspiled source code via npm.

es2015

{
  "main": "main.js",
  "es2015": "main-es2015.js"
}

Angular 定义的未转码的 es6 源码。

详细参考 https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/edit#.

esm

详细参考 adjusted proposal: ES module "esm": true package.json flag.

6. react-native 相关字段

react-native: 使用 react 组件技术写原生APP。

react-native

{
  "main": "./lib/main.js",
  "react-native": "./lib/main.react-native.js"
}

指定该模块供 react-native 使用的入口文件。

如果这个字段未定义,则回退到 main 字段指向的文件。

源代码查看.

7. webpack 相关字段

sideEffects

{
  "sideEffects": true|false
}

声明该模块是否包含 sideEffects(副作用),从而可以为 tree-shaking 提供更大的优化空间。

详细参考 sideEffects example, proposal for marking functions as pure, eslint-plugin-tree-shaking.

8. microbundle 相关字段

microbundle: 基于 rollup 零配置快速打包工具。

source

{
  "source": "src/index.js"
}

源文件入口文件。

详细参考 Specifying builds in package.json.

umd:main

{
  "umd:main": "dist/main.umd.js"
}

umd 模式 bundle 文件。

详细参考 Specifying builds in package.json.

8. parcel 相关字段

parcel: 零配置打包工具。

source

查看 parcel-bundler/parcel#1652.

9. babel 相关字段

babel: es6 -> es5 转码器。

babel

配置 babel

10. eslint 相关字段

eslint: js 代码检查与优化。

eslintConfig

配置 eslint

11. jest 相关字段

jest: js 测试库。

jest

{
  "jest": {
    "verbose": true
  }
}

配置 jest

详细参考 jest docs.

12. stylelint 相关字段

stylelint: style 代码检查与优化。

stylelint

配置 stylelint

详细参考 New configuration loader.

13. ava 相关字段

ava: js 测试库。

ava

{
  "ava": {
    "require": [ "@std/esm" ]
  }
}

配置 ava

详细参考 ava configuration.

14. nyc 相关字段

nyc: istanbul.js 命令行。

nyc

{
  "nyc": {
    "extension": [".js", ".mjs"],
    "require": ["@std/esm"]
  }
}

配置 nyc

详细参考 nyc docs.

15. CommonJS 保留字段

保留字段: build, default, email, external, files, imports, maintainer, paths, platform, require, summary, test, using, downloads, uid.

不可用字段: id, type, 以 _$ 开头的字段。

16. Standard JS 相关字段

Standard JS: js 代码检查与优化。

standard

{
  "standard": {
    "parser": "babel-eslint",
    "ignore": [
      "**/out/",
      "/lib/select2/",
      "/lib/ckeditor/",
      "tmp.js"
    ]
  }
}

配置 standard.

详细参考 https://standardjs.com/.

17. 其他

style

声明当前模块包含 style 部分,并指定入口文件。

支持的工具:

详细参考 Package.json "style" Attribute, istf-spec.

less

style 一样,但是是 less 文件。

支持的工具:

18. 更多

参考 package.json fields explained.

19. 后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

查看原文

赞 49 收藏 36 评论 1

大桔子 发布了文章 · 2020-05-13

直播弹幕滚动列表效果实现

pc版快手、移动端b站弹幕列表效果实现

一个原生js开发的横向弹幕
  • 效果如下:
    444.gif


  • 效果描述:页面上部为直播视频播放器,下半部分是弹幕列表,ui效果类似b站app分享出去的h5直播间,要实现的效果,当弹幕滚动到最底部的时候,新来的的弹幕会自动往上顶,如果向下滑动去看历史弹幕列表新来的弹幕则停止往上顶,然后左下角会出现新消息提示,当点击新消息提示则滚动到最底端新消息提示消失,或者手动滚动到最底端消息提示消失
  • 难点:手动滚动到最底部新消失提示消失(因为要监听scroll事件并在其中获取dom元素尺寸)
  • 误区:刚开始开发中,我的想法是弹幕列表最多存储 150 条数据,如果超出则新push一个,顶部就shift一个,这样有一个问题就是scroll事件是会不断的触发,在观察完pc端快手直播的弹幕列表,发现其大概原理是超出150条,则将前50条一次性去除,这样 scroll仅仅只会触发一次,再加上防抖操作,监听scroll事件的开销就很小了

  • 具体代码如下:
/**
 * h5 直播间弹幕列表组件
 * 
 * 使用方式
 * {liveInfo.danmuChannel && <DanmuPanel ref={ref => this.danmuRef = ref} wrapH={DANMUWRAPH}/> }
 * 父组件socket拿到数据通过组件 ref 实例调用 addDanmu(data)
 * 
 * 如需样式调整请自行修改less样式
 */

import React from "react";
import classnames from "classnames";
import { debounce, isScrollBottom } from "@/util/index"; // 自行实现或参考我下面的代码

import "./index.less";

interface propsType {
    wrapH?: string; // 弹幕容器高度
}

// 弹幕对象类型
type contentType = {
    name: string;
    content: string;
    key: string;
    reactId?: any; // 列表唯一标识,如不传会自动添加
}

export default class DanmuPanel extends React.Component<propsType, any> {
    danmuWrapHeight: number; // 弹幕容器dom高度
    danmuWrapRef: HTMLElement; // 弹幕容器dom
    danmuListRef: HTMLElement; // 弹幕列表dom
    restNums: number;
    reactId: number; // 弹幕列表 key
    debounceCb: Function;
    isBindScrolled: boolean;
    constructor(props) {
        super(props)
        this.state = {
            danmuList: [],
            restDanmu: 0,
        }
        this.restNums = 0;
        this.reactId = 0;
        this.debounceCb = debounce(this.danmuScroll, 200)
    }

    componentDidMount() {
        this.initDom();
        // this.testAddDanmu();
    }
    // 测试代码,后期删掉
    testAddDanmu() {
        let i = 0;
        setInterval(() => {
            ++i
            this.addDanmu({
                name: i + '-我是名字',
                content: i + '-我是内容',
                key: "danmu",
                reactId: i
            })
        }, 100)
    }

    private initDom() {
        this.danmuWrapRef = document.querySelector('.danmu-wrap');
        this.danmuListRef = document.querySelector(".danmu-wrap .list");
        this.danmuWrapHeight = this.danmuWrapRef.offsetHeight;
    }

    private addScroll = () => {
        this.debounceCb();
        this.isBindScrolled = true;
    }

    // 弹幕列表滚动到底部回调
    private danmuScroll = () => {
        const ele: HTMLElement = this.danmuWrapRef;
        const isBottom = isScrollBottom(ele, ele.clientHeight);
        if (isBottom) {
            this.restNums = 0;
            this.setState({ restDanmu: 0 })
        }
    }

    // 供父组件调用 socket拿到结果后调用
    public addDanmu = (data: contentType) => {
        const { danmuList } = this.state;
        data.reactId = ++this.reactId;
        if (danmuList.length >= 150) {
            danmuList.splice(0, 50)
        };
        danmuList.push(data);
        this.setState({ danmuList }, this.renderDanmu)
    }

    private renderDanmu = () => {
        const listH = this.danmuListRef.offsetHeight;
        const diff = listH - this.danmuWrapHeight;
        const top = this.danmuWrapRef.scrollTop;
        if (diff - top < 50) {
            if (diff > 0) {
                if (this.isBindScrolled) {
                    this.isBindScrolled = false;
                    this.danmuWrapRef.removeEventListener('scroll', this.addScroll)
                }
                this.danmuWrapRef.scrollTo({ top: diff + 40, left: 0, behavior: 'smooth' });
                this.restNums = 0;
            }
        } else {
            ++this.restNums;
            if (!this.isBindScrolled) {
                this.isBindScrolled = true;
                this.danmuWrapRef.addEventListener('scroll', this.addScroll)
            }
        }
        this.setState({ restDanmu: this.restNums >= 99 ? '99+' : this.restNums });
    }

    private scrollBottom = () => {
        this.restNums = 0;
        this.setState({ restDanmu: this.restNums })
        this.danmuWrapRef.scrollTo({ top: this.danmuListRef.offsetHeight, left: 0, behavior: 'smooth' });
    }

    render() {
        const { danmuList, restDanmu } = this.state;
        return (
            <div className="danmu-panel">
                <div className="danmu-wrap" style={{ height: this.props.wrapH }}>
                    <ul className="list">
                        {
                            danmuList.map((v) => (
                                <li key={v.reactId}>
                                    <span className="name">{v.name}:</span>
                                    <span className={classnames("content", v.key)}>{v.content}</span>
                                </li>
                            ))
                        }
                    </ul>
                </div>
                {
                    !!restDanmu &&
                    <div className="rest-nums" onClick={this.scrollBottom}>{ restDanmu }条新消息</div>
                }
            </div>
        )
    }
}
  • debounce isScrollBottom 实现如下

/**
 * 防抖函数
 * @param fn 
 * @param wait 
 */
export function debounce(fn:Function, wait:number = 500) {
    let timeout:number = 0;
    return function () {
        // 每次触发 scroll handler 时先清除定时器
        clearTimeout(timeout);
        // 指定 xx ms 后触发真正想进行的操作 handler
        timeout = setTimeout(fn, wait);
    };
};


/**
 * 是否滚到到容器底部
 * @param ele 滚动容器
 * @param wrapHeight 容器高度
 */
export function isScrollBottom(ele: HTMLElement, wrapHeight:number, threshold: number = 30) {
    const h1 = ele.scrollHeight - ele.scrollTop;
    const h2 = wrapHeight + threshold;
    const isBottom = h1 <= h2;
    // console.log('-->', isBottom, ele.scrollHeight, ele.scrollTop, h2)
    return isBottom;
}
查看原文

赞 2 收藏 1 评论 0

大桔子 关注了问题 · 2020-05-04

vuecli中引用static的问题

各位大侠,我在网上学习的,有些图片要放在static目录下,所以我引用图片的代码是<img data-original='/static/tu.jpg'
,在代码打包后static文件夹和index.html在一个目录下,是可以正常引用的。
但我目前是在开发环境下,例如我的一个组件需要引用图片,他在src/components文件夹下,那我引用的代码用<img data-original='/static/tu.jpg'就不能正常显示了,我也看不到图片的效果了,我还要改成<img data-original='。。/。。/static/tu.jpg'格式。这样我的图片很多,好麻烦啊,请问大家平常是怎么搞的?

关注 4 回答 3

认证与成就

  • 获得 106 次点赞
  • 获得 24 枚徽章 获得 1 枚金徽章, 获得 7 枚银徽章, 获得 16 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-08-26
个人主页被 3.3k 人浏览