littledu

littledu 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

littledu 关注了用户 · 2018-08-23

冴羽 @yayu

JavaScript深入系列 15 篇已完结。

JavaScript专题系列 20 篇已完结。

underscore 系列 8 篇已完结。

ES6 系列 20 篇已完结。

现在写起了各种单篇……

React 系列还在懒惰中……

公众号:冴羽的JavaScript博客

关注 2030

littledu 赞了文章 · 2016-10-12

大公司是怎么发布静态资源的

静态资源(Static Resources): js、css、img 等非服务器动态运行生成的文件。

在一般的网站中,静态资源使用频率高,流量占用大。对于有追求(访问量稍大)的网站,都会把静态资源放置到 CDN 服务器,不占用业务服务器的网络带宽,而达到更好的用户体验。那大公司的静态资源是怎么发布的呢?

发布要求

1. 静态资源必须走 CDN

如非自建 CDN,推荐 又拍云,阿里云,七牛云。

2. 平滑升级--非覆盖式发布

在不修改线上静态资源的情况下,发布新的静态资源。即 CDN 上的静态资源必须共存多个静态资源版本

负责百度 FIS 的核心功能开发者张云龙已经给出了 部署的方案非覆盖式发布。他的文中提到:

  1. 先部署页面,再部署资源:在二者部署的时间间隔内,如果有用户访问页面,就会在新的页面结构中加载旧的资源,并且把这个旧版本的资源当做新版本缓存起来,其结果就是:用户访问到了一个样式错乱的页面,除非手动刷新,否则在资源缓存过期之前,页面会一直执行错误。

  2. 先部署资源,再部署页面:在部署时间间隔之内,有旧版本资源本地缓存的用户访问网站,由于请求的页面是旧版本的,资源引用没有改变,浏览器将直接使用本地缓存,这种情况下页面展现正常;但没有本地缓存或者缓存过期的用户访问网站,就会出现旧版本页面加载新版本资源的情况,导致页面执行错误,但当页面完成部署,这部分用户再次访问页面又会恢复正常了。

3. 精确的缓存控制——文件版本号

更新静态资源,只更新修改的部分。不能修改未更新的资源的引用。

在 CDN 的配置中,会把缓存的时间设置很长。文件的更新就需要一种灵活的机制,也不能每次发布版本就把所有文件更新一遍,全量更新静态资源对于用户和服务提供者来说都是非常不友好的,极度浪费 CDN 资源和用户的流量。
比如: a.html 页面引用了 100 个静态资源文件,但是本次发版本只修改了一个a.js文件。那么就只能修改 a.js 文件的引用,可能修改为 a.hash.js

解决办法:根据文件的内容算出一个值,如果文件内容不改变,那么这个值就不会改变。

张云龙的文章中给出了『精确的缓存控制』的原理————我们会很自然的联想到利用 数据摘要要算法 对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。

Grunt,gulp 也有对应的插件用来解决这个问题。如果你使用 webpack,那么可以配置下 chunkhash 或者 hash。更厉害的,文章末尾还有相关的解决方案。

发布和问题

1. 发布流程

静态资源发布流程图

如果我们把静态资源的实际访问域名设置为 res.company.com

  1. 功能开发结束后,开发人员把静态资源提交到测试环境 Git 仓库。

  2. 测试人员在测试环境通过绑定 host 的方式测试,测试通过后,进入发布阶段,需要静态资源代码提交到产品环境 Git 仓库。(使用测试和产品两个仓库,是为了测试和产品环境完全隔离)

  3. 源站(origin.res.company.com)从产品环境 Git 拉取静态资源的代码,并且为静态资源提供访问服务(nginx 等),该域名不提供文件缓存服务,仅仅作为 CDN 的回源地址。所以代码中一定不要写源站的信息。

  4. 在 CDN 服务商提供的空间中,将回源地址配置为源站(origin.res.company.com)。

  5. CDN 服务器会提供一个默认的访问域名,比如 cdn-1.res.cdn_company.com, 然后把实际需求访问的静态资源域名 res.company.com CNAME 到 cdn-1.res.cdn_company.com

  6. 代码发布完毕。用户访问的时候,会访问到最近的 CDN 阶段。CDN 结点去文件中心取文件,如果不存在文件则回源到源站,获取文件。(有些 CDN 产品提供文件存储功能,异地备份)

  7. 通过这样的流程,就能让用户访通过 CDN 访问到我们的实际资源。

2. 怎么做到多个项目共存

结论:我们会按照目录来划分业务的静态资源。

res.company.com 域名下面会共存多个业务。A 业务、B 业务都是使用同样的发布方案。
比如某静态资源的访问路径为:http://res.company.com/:业务名/js/test.de5b0b0c.js

多个团队可以使用同一个静态资源仓库,res.git,然后按照目录来区分业务。web 服务器直接指向到 res/即可。静态资源更新只需要 pull 代码即可,而不需求做其他改动。

3. 是否同步源站文件到 CDN

结论:同步源站文件到 CDN 能确保实现非覆盖式发布。

又拍云等 CDN 服务商都提供了这些贴心的服务,就算你提交了新的同名文件,那也是不会生效的。

CDN 回源

  1. 终端用户就近访问 CDN 加速节点上的资源

  2. 若 CDN 加速节点上不存在资源,则回源获取资源

  3. 回源获取的资源持久化存储到 CDN

  4. 返回资源内容给终端用户

  5. 当终端用户再次访问相同的资源时,CDN 的加速节点将直接返回资源内容,不需要重新回源获取

另外我还做了测试,在未做源站迁移的情况下,删掉源站文件,会出现大面积的 404 响应。
未做源站迁移

推荐工具

  1. 超厉害的 CDN 服务商——又拍云

  2. 微信出品的前端工程化工具——tmt-workflow

  3. 百度团队出品——FIS

  4. 京东的非官方——JDF

  5. 能打造最牛逼的工程化工具——webpack

参考文档

  1. 大公司里怎样开发和部署前端代码?

  2. 前端工程精粹(一):静态资源版本更新与缓存

  3. 又拍云【功能介绍】源站资源迁移

查看原文

赞 34 收藏 162 评论 5

littledu 赞了文章 · 2016-08-21

【二次元的CSS】—— 用 DIV + CSS3 画大白(详解步骤)

原本自己也想画大白,正巧看到一位同学(github:https://github.com/shiyiwang)也用相同的方法画了。 且细节相当到位。所以我就fork了一下,在此我也分享一下。
同时,我也希望有更多的同学发挥自己的想象力,来找个东西画画。

图片描述

如果看过我前几次的分享,肯定能马上想到大白的各个部位是怎么实现的。

GitHub传送门:https://github.com/lancer07/css3_Baymax

第一步:头

图片描述

<div class="baymax-head">
  <div class="head-highlight"></div>
  <div class="baymax-eyes"></div>
</div>
.baymax-head {
  position: absolute;
  left: 50%;
  margin-left: -21px;
  width: 42px;
  height: 28px;
  -webkit-border-radius: 50%;
  -moz-border-radius: 50%;
  -ms-border-radius: 50%;
  border-radius: 50%;
  -webkit-transform: rotate(5deg);
  -moz-transform: rotate(5deg);
  -ms-transform: rotate(5deg);
  -o-transform: rotate(5deg);
  transform: rotate(5deg);
  z-index: 3;
  overflow: hidden;
  -webkit-box-shadow: 0 6px 8px -5px rgba(128, 128, 128, 0.75), inset 0 -6px 8px -5px rgba(204, 204, 204, 0.5);
  -moz-box-shadow: 0 6px 8px -5px rgba(128, 128, 128, 0.75), inset 0 -6px 8px -5px rgba(204, 204, 204, 0.5);
  box-shadow: 0 6px 8px -5px rgba(128, 128, 128, 0.75), inset 0 -6px 8px -5px rgba(204, 204, 204, 0.5);
}

.baymax-head .head-highlight {
  position: absolute;
  top: 12%;
  right: 25%;
  width: 45%;
  height: 1%;
  -webkit-border-radius: 50%;
  -moz-border-radius: 50%;
  -ms-border-radius: 50%;
  border-radius: 50%;
  background: #ffffff;
  -webkit-box-shadow: 0 0 18px 9px #ffffff, 0 0 0 0 transparent;
  -moz-box-shadow: 0 0 18px 9px #ffffff, 0 0 0 0 transparent;
  box-shadow: 0 0 18px 9px #ffffff, 0 0 0 0 transparent;
}

.baymax-head .baymax-eyes {
  position: relative;
  top: 10px;
  left: 50%;
  -webkit-transform: translateX(-10px);
  -moz-transform: translateX(-10px);
  -ms-transform: translateX(-10px);
  -o-transform: translateX(-10px);
  transform: translateX(-10px);
  height: 1px;
  width: 20px;
  background: #333333;
}

.baymax-head .baymax-eyes:before, .baymax-head .baymax-eyes:after {
  top: -3px;
  width: 6px;
  height: 6px;
  background: #333333;
  -webkit-border-radius: 50%;
  -moz-border-radius: 50%;
  -ms-border-radius: 50%;
  border-radius: 50%;
}

.baymax-head .baymax-eyes:before {
  left: -2px;
}

.baymax-head .baymax-eyes:after {
  right: -2px;
}

第二步:身体

图片描述

<div class="baymax-body">
  <div class="body-highlight"></div>
  <div class="baymax-heart"></div>
</div>
.baymax-body {
  position: absolute;
  top: 18.66667px;
  left: 50%;
  -webkit-transform: translateX(-62px);
  -moz-transform: translateX(-62px);
  -ms-transform: translateX(-62px);
  -o-transform: translateX(-62px);
  transform: translateX(-62px);
  width: 124px;
  height: 180px;
  -webkit-border-radius: 50% 50% 50% 50%/60% 60% 40% 40%;
  -moz-border-radius: 50% 50% 50% 50%/60% 60% 40% 40%;
  -ms-border-radius: 50% 50% 50% 50%/60% 60% 40% 40%;
  border-radius: 50% 50% 50% 50%/60% 60% 40% 40%;
  overflow: hidden;
  z-index: 2;
}

.baymax-body .body-highlight {
  position: absolute;
  top: 20%;
  right: 40%;
  width: 0%;
  height: 50%;
  -webkit-border-radius: 50%;
  -moz-border-radius: 50%;
  -ms-border-radius: 50%;
  border-radius: 50%;
  background: #ffffff;
  -webkit-box-shadow: 0 0 45px 25px #ffffff, 0 0 0 0 transparent;
  -moz-box-shadow: 0 0 45px 25px #ffffff, 0 0 0 0 transparent;
  box-shadow: 0 0 45px 25px #ffffff, 0 0 0 0 transparent;
}

.baymax-body .baymax-heart {
  position: absolute;
  top: 35px;
  right: 30%;
  height: 12px;
  width: 12px;
  -webkit-border-radius: 50%;
  -moz-border-radius: 50%;
  -ms-border-radius: 50%;
  border-radius: 50%;
}

.baymax-body:before, .baymax-body:after {
  top: 55px;
  width: 15px;
  height: 40px;
}

.baymax-body:before {
  left: -8px;
  -webkit-transform: rotate(15deg);
  -moz-transform: rotate(15deg);
  -ms-transform: rotate(15deg);
  -o-transform: rotate(15deg);
  transform: rotate(15deg);
}

.baymax-body:after {
  right: -8px;
  -webkit-transform: rotate(-15deg);
  -moz-transform: rotate(-15deg);
  -ms-transform: rotate(-15deg);
  -o-transform: rotate(-15deg);
  transform: rotate(-15deg);
}

第三步:双手

图片描述

<div class="baymax-arm baymax-left-arm">
   <div class="arm-highlight larm-highlight"></div>
</div>
<div class="baymax-arm baymax-right-arm">
   <div class="arm-highlight rarm-highlight"></div>
</div>
.baymax-arm {
  position: absolute;
  top: 28px;
  height: 145px;
  width: 40px;
  overflow: hidden;
  z-index: 1;
}

.baymax-arm:after {
  top: 60px;
  height: 50px;
  width: 25px;
}

.baymax-arm .arm-highlight {
  position: absolute;
  top: 25%;
  width: 1%;
  height: 65%;
  -webkit-border-radius: 50%;
  -moz-border-radius: 50%;
  -ms-border-radius: 50%;
  border-radius: 50%;
  background: #ffffff;
  -webkit-box-shadow: 0 0 16px 7px rgba(255, 255, 255, 0.8), 0 0 0 0 transparent;
  -moz-box-shadow: 0 0 16px 7px rgba(255, 255, 255, 0.8), 0 0 0 0 transparent;
  box-shadow: 0 0 16px 7px rgba(255, 255, 255, 0.8), 0 0 0 0 transparent;
}

.baymax-left-arm {
  left: 65px;
  -webkit-transform: rotate(25deg);
  -moz-transform: rotate(25deg);
  -ms-transform: rotate(25deg);
  -o-transform: rotate(25deg);
  transform: rotate(25deg);
  -webkit-border-radius: 90px 20px 20px 90px/200px 40px 40px 200px;
  -moz-border-radius: 90px 20px 20px 90px/200px 40px 40px 200px;
  -ms-border-radius: 90px 20px 20px 90px/200px 40px 40px 200px;
  border-radius: 90px 20px 20px 90px/200px 40px 40px 200px;
}

.baymax-left-arm:after {
  left: -20px;
}

.baymax-left-arm .larm-highlight {
  left: 30%;
}

.baymax-right-arm {
  right: 65px;
  -webkit-transform: rotate(-25deg);
  -moz-transform: rotate(-25deg);
  -ms-transform: rotate(-25deg);
  -o-transform: rotate(-25deg);
  transform: rotate(-25deg);
  -webkit-border-radius: 20px 90px 90px 20px/40px 200px 200px 40px;
  -moz-border-radius: 20px 90px 90px 20px/40px 200px 200px 40px;
  -ms-border-radius: 20px 90px 90px 20px/40px 200px 200px 40px;
  border-radius: 20px 90px 90px 20px/40px 200px 200px 40px;
}

.baymax-right-arm:after {
  right: -20px;
}

.baymax-right-arm .rarm-highlight {
  right: 30%;
}

第四步:双腿

图片描述

<div class="baymax-leg baymax-left-leg">
    <div class="leg-highlight lleg-highlight"></div>
    <div class="crosspart"></div>
</div>
<div class="baymax-leg baymax-right-leg">
    <div class="leg-highlight rleg-highlight"></div>
    <div class="crosspart"></div>
</div>
.baymax-leg {
  position: absolute;
  top: 165px;
  width: 48px;
  height: 85px;
  overflow: hidden;
  z-index: 1;
}

.baymax-leg:before {
  top: -50px;
  height: 100px;
  width: 30px;
}

.baymax-leg:after {
  bottom: -22px;
  height: 30px;
  width: 60px;
}

.baymax-leg .leg-highlight {
  position: absolute;
  top: 40%;
  width: 1%;
  height: 38%;
  -webkit-border-radius: 50%;
  -moz-border-radius: 50%;
  -ms-border-radius: 50%;
  border-radius: 50%;
  background: #ffffff;
  -webkit-box-shadow: 0 0 16px 7px rgba(255, 255, 255, 0.8), 0 0 0 0 transparent;
  -moz-box-shadow: 0 0 16px 7px rgba(255, 255, 255, 0.8), 0 0 0 0 transparent;
  box-shadow: 0 0 16px 7px rgba(255, 255, 255, 0.8), 0 0 0 0 transparent;
}

.baymax-leg .crosspart {
  content: "";
  position: absolute;
  top: 0;
  width: 48px;
  height: 85px;
  -webkit-box-shadow: inset 0px 0px 15px 0px #cccccc, 0 0 0 0 transparent;
  -moz-box-shadow: inset 0px 0px 15px 0px #cccccc, 0 0 0 0 transparent;
  box-shadow: inset 0px 0px 15px 0px #cccccc, 0 0 0 0 transparent;
}

.baymax-left-leg {
  left: 50%;
  margin-left: -50px;
  -webkit-border-radius: 20% 0 30% 50%/50% 0 30% 50%;
  -moz-border-radius: 20% 0 30% 50%/50% 0 30% 50%;
  -ms-border-radius: 20% 0 30% 50%/50% 0 30% 50%;
  border-radius: 20% 0 30% 50%/50% 0 30% 50%;
}

.baymax-left-leg:before {
  left: -20px;
}

.baymax-left-leg:after {
  left: 0;
}

.baymax-left-leg .lleg-highlight {
  left: 25px;
  -webkit-transform: rotate(-5deg);
  -moz-transform: rotate(-5deg);
  -ms-transform: rotate(-5deg);
  -o-transform: rotate(-5deg);
  transform: rotate(-5deg);
}

.baymax-right-leg {
  right: 50%;
  margin-right: -50px;
  -webkit-border-radius: 0 20% 50% 30%/0 50% 50% 30%;
  -moz-border-radius: 0 20% 50% 30%/0 50% 50% 30%;
  -ms-border-radius: 0 20% 50% 30%/0 50% 50% 30%;
  border-radius: 0 20% 50% 30%/0 50% 50% 30%;
}

.baymax-right-leg:before {
  right: -20px;
}

.baymax-right-leg:after {
  right: 0;
}

.baymax-right-leg .rleg-highlight {
  right: 20px;
  -webkit-transform: rotate(5deg);
  -moz-transform: rotate(5deg);
  -ms-transform: rotate(5deg);
  -o-transform: rotate(5deg);
  transform: rotate(5deg);
}

欢迎大家吐槽

查看原文

赞 3 收藏 17 评论 2

littledu 回答了问题 · 2016-07-20

解决怎样同时启动gulp和json-server服务?

一个服务占用了一个端口,另一个肯定是不行的,你之所以想要同一个端口,只是为了解决跨域,那应该将另一个端口跨域访问那个头信息设置设成 *就可以了

关注 9 回答 4

littledu 回答了问题 · 2016-07-09

解决gulp browser-sync报错

很明显模块没安装完全,重新 npm install 或者用 cnpm

关注 3 回答 2

littledu 关注了问题 · 2016-07-08

从github上下载的项目怎么整合到自己的项目里面。

公司有一个需求是要打印文件的功能,然后刚好在github里面有这个功能的源码,下下来以后发现不知道怎么整合到自己的项目里面去。有以下几个问题:
1.从github上面下载的项目的前端构建工具是gulp,但是我自己的是grunt.
2.前端框架我的用的是angular,而我下的项目我根本就不知道用的是什么框架。
3.下好的项目代码有点多,有点乱,更本就不知道要找哪些东西。而且后端的语言可能也不一样。我的后端是node.js,而下好的项目的后端是什么我没有看出来。
这是项目地址:https://github.com/mozilla/pdf.js
以下是几张图片:

图片描述

图片描述

图片描述

关注 4 回答 3

littledu 回答了问题 · 2016-07-08

从github上下载的项目怎么整合到自己的项目里面。

看了下 readme, 你可以先按他说明的先 build 一份出来,然后复制到你自己的项目下去引用,就可以用了。

关注 4 回答 3

littledu 赞了文章 · 2016-06-26

实践 HTML5 的 CSS3 Media Queries

先来介绍下 media,确切的说应该是 CSS media queries(CSS 媒体查询),媒体查询包含了一个媒体类型和至少一个使用如宽度、高度和颜色等媒体属性来限制样式表范围的表达式。CSS3 加入的媒体查询使得无需修改内容便可以使样式应用于某些特定的设备范围。

那么该怎么定义 media 呢,看下面的代码,你肯定能猜出个大概。

<!-- link元素中的CSS媒体查询 -->
<link rel="stylesheet" media="(max-width: 800px)" href="example.css" />

<!-- 样式表中的CSS媒体查询 -->
<style>
@media (max-width: 600px) {
  .facet_sidebar {
    display: none;
  }
}
</style>

关于解释,文档中是这么说的,当媒体查询为真时,相关的样式表或样式规则就会按照正常的级联规则被应用。即使媒体查询返回假, <link> 标签上带有媒体查询的样式表仍将被下载(只不过不会被应用)。

所以呢,这也是一种弊端,如果说对某个页面定义了多个样式标准来因对不同的 media 属性的话,那在页面的加载时间将会受到影响,但是话有说回来,在当前网络快速发展的时代,网速也在不断地完善和提高,因此影响并不大,几乎可以忽略不计。

media 还可以通过逻辑操作符(and、not、only 等)来组成 media 表达式,书写更复杂的过滤条件,这些表达式我就不再这边一一说明了,想深入了解的同学,可以阅读相关的说明文档:https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries 这里面有做详细的介绍。

接下来我们来用几个 Demo 来演示下 media 的用法及表现。

既然我们今天的目的是探讨如何监听 devicePixelRatio 属性的变化,那么我们就以在不同的 devicePixelRatio 值情况下,来改变某个 div 的 background 样式,具体的代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <style media="screen">
            @media screen and (min-resolution: 2dppx) {
                #image {
                    background : red;
                }
            }
            @media screen and (min-resolution: 1dppx) {
                #image {
                background: #000;
            }
            }
        </style>
    </head>
    <body>
        <div id="image" style="width:100px; height:100px"></div>
    </body>
</html>

代码有了,那么要怎么测试呢?在一般情况下,devicePixelRatio 属相是不会变化的,但是肯定会存在特殊情况的,就比如说,你的电脑接了两个显示器,而且两台浏览器的 devicePixelRatio 属性是不一样的,那么恭喜你,你已经具备测试条件,只需要将页面从一个屏拖到另外一个屏,这样你就可以看到效果了。

有去测试的同学会发现,div 的背景色并没有想代码中设置的那样,在不同的 devicePixelRatio 属性值下,展现出不同的颜色,这是为什么呢?

这代码是我最开始写代码,运行后发现没效果,起初我也不知道原因,在跨屏拖动页面的时候,在浏览器控制台中,我找到了原因。那么到底是什么原因导致设置无效的呢?我们来看看两个屏幕下的 Style 内容截图,左边是 min-resolution 等于 1,右边是等于 2

591709-20160621014839022-913784671.png591709-20160621014847475-1463577990.png

对比着两个图,可以发现,在 min-resolution 等于 2 的情况下,在里面定义的属性被覆盖掉了,并没有生效,这是为什么呢?

要解释的话,这里恐怕需要补充一点知识,就是关于 min- 和 max- 的前缀,在代码中的所起到的具体效果,在文档中是这么描述的:大多数媒体属性带有 “min-” 和 “max-” 前缀,用于表达 “大于等于” 和 “小于等于”。这避免了使用与HTML和XML冲突的 “<” 和 “>” 字符。如果你未向媒体属性指定一个值,并且该特性的实际值不为零,则该表达式被解析为真。如果浏览器运行的设备上没有该属性值,包含这个属性值的表达式一般返回假。

其实上面的说明已经帮我解释清楚了,我再通俗地和大家解释一下:当 devicePixelRatio 为 1 时,只有 min-resolution: 1dppx 这个条件满足,因此 div 的颜色是黑色没错;当 devicePixelRatio 为 2 时,两个 media 都满足条件,同时 CSS 的规则是后加载的样式将会覆盖先加载的样式,由于我么将 min-resolution: 1dppx 的 media 写在后面,因此如果两个 media 都满足条件的话, min-resolution: 1dppx 的 media 将会覆盖 min-resolution: 2dppx 的 media,因此不管你把页面拖到那个屏幕,那个 div 的背景色都是黑色。

那么我们将两个 media 调换一下位置,问题就顺利地解决了。

<style media="screen">
    @media screen and (min-resolution: 1dppx) {
        #image {
            background: #000;
        }
    }
    @media screen and (min-resolution: 2dppx) {
        #image {
            background : red;
        }
    }
</style>

以上是根据不同的 media 条件设置不同的样式,这是 CSS 的做法,在 JavaScript 中,没有专门的方法来监听 window.devicePixelRatio 属性变化,那么该怎么监听 devicePixelRatio 属性的变化呢?方法也很简单,看看下面的代码,你一定就明白了:

window.matchMedia('screen and (min-resolution: 2dppx)').addListener(function(e) {
    console.info(e, window.devicePixelRatio);
});

稍微解释下,通过 window.matchMedia(‘media expression’) 方法获取到对应的 media,然后通过 addListener(function(e) {}) 来监听 media 的变化。

有玩过 Canvas 的朋友一定知道,要想绘制出来的内容效果最佳的话,Canvas 自身的 width 和 height 属性值与 style 中的 width 和 height 的比例应该恰好等于 devicePixelRatio 的值,所有如果你在切换不同 devicePixelRatio 属性值的屏幕时,没有重新设置 Canvas 的宽高的话,绘制出来的画面将不是最佳的效果。

接下来我们基于 HT for Web 的 3D 模型来做一个小实验。实验的内容是这样的,在 GraphView 中有一辆车根据某条路线前行,当拖到另外一个屏幕的时候,换辆车子。先来看看效果图:

591709-20160621015606006-992760480.png

591709-20160621015616897-240297018.png

上面两张图分别是在不同的屏幕中的截图,车子动起来的效果可以访问以下链接:

实验的地址是:http://www.hightopo.com/demo/media/index.html 以下是实验的具体代码:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>HT for Web</title>
        <style media="screen">
            @media screen and (min-resolution: 2dppx) {}
            html, body {
                padding: 0px;
                margin: 0px;
            }
        </style>
        <script data-original="../../oldhtforweb/lib/core/ht.js"></script>
        <script data-original="../../oldhtforweb/lib/plugin/ht-modeling.js"></script>
        <script data-original="../../oldhtforweb/lib/plugin/ht-obj.js"></script>
        <script>
            ht.Default.setImage('road', './images/road.jpg');
            var init = function() {
                g3d = new ht.graph3d.Graph3dView();
                var dm = g3d.dm();
                g3d.addToDOM();
                g3d.setEye(1200, 300, 0);
                g3d.getNote = function(data) {
                    if (data.getTag() !== 'carNode') return null;
                    return 'DevicePixelRatio : ' + window.devicePixelRatio;
                };

                var carIndex = 0;
                window.matchMedia('screen and (min-resolution: 2dppx)').addListener(function() {
                    carIndex = (carIndex + 1) % 2;
                    var obj = result[carIndex];
                    carNode.s('shape3d', obj.name);
                    ht.Default.setDevicePixelRatio();
                });

                var polyline = createPath(dm, 300),
                    params = {
                        delay: 0,
                        duration: 10000,
                        easing: function(t){
                            return (t *= 2) < 1 ? 0.5 * t * t : 0.5 * (1 - (--t) * (t - 2));
                        },
                        action: function(v, t){
                            var length = g3d.getLineLength(polyline);
                            var offset = g3d.getLineOffset(polyline, length * v),
                                point = offset.point,
                                px = point.x,
                                py = point.y,
                                pz = point.z,
                                tangent = offset.tangent,
                                tx = tangent.x,
                                ty = tangent.y,
                                tz = tangent.z;
                            carNode.p3(px, py - 9, pz);
                            carNode.lookAt([px + tx, py + ty - 9, pz + tz], 'front');
                        },
                        finishFunc: function(){
                            ht.Default.startAnim(params);
                        }
                    },
                    carList = [ 'fordFocus', 'concept-sedan-01v2'],
                    result = [], carNode = new ht.Node();
                carNode.setTag('carNode');
                carList.forEach(function(name, index) {
                    ht.Default.loadObj('./objs/'+name+'/'+name+'.obj', './objs/'+name+'/'+name+'.mtl', {
                        cube: true,
                        center: true,
                        shape3d: name,
                        finishFunc: function(modelMap, array, rawS3) {
                            var k = 110 / rawS3[0];
                            rawS3 = rawS3.map(function(v) { return v * k; });
                            result[index] = {
                                'name' : name,
                                'modelMap' : modelMap,
                                'array' : array,
                                'rawS3' : rawS3
                            };
                            if (index === 0) {
                                var node = carNode;
                                node.s({
                                    'wf.width' : 0,
                                    'shape3d' : name,
                                    'note.position' : 44,
                                    'note' : 'DevicePixelRatio : ' + window.devicePixelRatio,
                                    'note.face' : 'top',
                                    'note.autorotate' : true,
                                    'note.font' : '46px arial, sans-serif'
                                });
                                node.s3(rawS3);
                                node.r3(0, Math.PI, 0);
                                dm.add(node);
                                polyline.setElevation(rawS3[1] * 0.5 + 2);
                                ht.Default.startAnim(params);
                            }
                        }
                    });
                });
            };

            var createPath = function(dm, radius) {
                var polyline = new ht.Polyline();
                polyline.setThickness(2);

                polyline.s({
                    'shape.border.pattern': [16, 16],
                    'shape.border.color': 'rgba(0, 0, 0, 0)',
                    'shape3d.resolution': 300,
                    '3d.selectable': false
                });
                dm.add(polyline);

                var cx = 0,
                    cy = radius * Math.PI * 0.5,
                    count = 500,
                    points = [{ x: radius, y: -cy, e: 0 }],
                    segments = [1];
                for (var k = 0; k < count + 1; k++) {
                    var angle = k * Math.PI / count;
                    points.push({
                        x: cx + radius * Math.cos(angle),
                        y: cy + radius * Math.sin(angle),
                        e: 0
                    });
                    segments.push(2);
                }

                cy *= -1;
                radius *= -1;
                for (var k = 0; k < count + 1; k++) {
                    var angle = k * Math.PI / count;
                    points.push({
                        x: cx + radius * Math.cos(angle),
                        y: cy + radius * Math.sin(angle),
                        e: 0
                    });
                    segments.push(2);
                }

                polyline.setPoints(points);
                polyline.setSegments(segments);

                var shape = new ht.Shape();
                shape.setPoints(points);
                shape.setSegments(segments);
                shape.s({
                    'top.visible' : false,
                    'bottom.image' : 'road',
                    'bottom.reverse.flip' : true,
                    'bottom.uv.scale' : [13, 1],
                    'back.visible' : false,
                    'front.reverse.flip' : true,
                    '3d.selectable': false
                });
                shape.setThickness(180);
                shape.setTall(15);
                shape.setClosePath(true);
                dm.add(shape);

                return polyline;
            };
        </script>
    </head>
    <body onload="init();">
    </body>
</html>

来介绍下这次 Demo 中都用到的了 HT for Web 的那些技术。

首先是车子,车子并不是通过 HT for Web 生成的,而是通过专业的 3D 工具设计,然后导出 obj 和 mtl 文件,HT for Web 对 obj 和 mtl 文件进行解析,然后显示在 Graph3dView 中,更多具体的介绍可以查阅我么的 obj 文档:http://www.hightopo.com/guide/guide/plugin/obj/ht-obj-guide.html

在 obj 文档中,你会看到一个一个飞机的例子,飞机沿着设定好的路线飞行,你应该会想,这个寻路是怎么实现的呢?其实很简单,我们将路线切割成一个个很小很小的单元,然后根据算法依次获取到小单元的坐标设置到移动的物体上,这样物体就动起来了。

在 Demo 中,有一条很精致的马路,这条马路就是一个 Shape 节点,根据车的路径生成的马路,Shape 是一个六面体,因为首尾相连了,所以没有左右面,在这个例子中,我将马路的 back 和 top 面隐藏了,然后 bottom 面支持翻转,让 bottom 面的贴图显示在内表面上,这样马路就建成了。

查看原文

赞 1 收藏 14 评论 0

littledu 发布了文章 · 2016-06-17

用 Electron 打造跨平台前端 App

前言

现如今,用 HTML、JavaScript、CSS、Node.js 写桌面应用早已不是什么新鲜的事了,作为一名前端,能够使用自己熟悉的语言,快速实现自己想要的桌面应用,是件很让人兴奋的事。
目前常见的有 NWheXElectron。今天,就来简单的上手一下 Electron。

Electron 是什么?

Electron 是一款可以利用 Web技术 开发跨平台桌面应用的框架,最初是 Github 发布的 Atom 编辑器衍生出的 Atom Shell,后更名为 Electron。

Electron 能做什么?

Electron 内置了 Chromium 内核 和 Node,因此可以使用 HTML 和 CSS 来实现应用的 GUI 界面,用 JavaScript 调用丰富的原生 API 实现桌面应用。你也可以将 Electron 看作是一个由 JavaScript 控制的一个小型的 Chrome 内核浏览器。

由于内置的 Chromium 内核 和 Node, 因此我们不需要关心前端的兼容问题,你甚至可以写 -webkit- only 的代码; 也不需要关心一些需要编译的 Node 模块兼容问题,因为 Node 版本是固定的。因此,用 Electron 来编写跨平台应用程序是非常合适的。

或许你还不知道,Visual Studio Codewordpressslack 等客户端都是基于 Electron 开发的。

下面,先快速上手一下。

快速入门

相信你看到这里都是对 Node 有一定了解的,故这里不再对 Node 的安装进行描述。

我们有如下目录结构:

electron-quick-start/
    ├── package.json
    ├── main.js
    └── index.html

package.json 跟常规 Node 程序一致,将 main.js 作为 程序的启动入口文件,基本内容如下:

{
  "name"    : "electron-quick-start",
  "version" : "1.0.0",
  "main"    : "main.js",
  "scripts" : {
    "start" : "electron main.js"
  },
  "devDependencies": {
    "electron-prebuilt": "^1.2.0"
  }
}

我们用 index.html 作为我们的程序界面,简单的界面代码如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>

接着是最重要的入口文件 main.js 的编写了,其内容如下:

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
let mainWindow;

function createWindow () {
  //创建一个 800x600 的浏览器窗口
  mainWindow = new BrowserWindow({width: 800, height: 600});

  //加载应用的界面文件
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  //打开开发者工具,方便调试
  //mainWindow.webContents.openDevTools();

  mainWindow.on('closed', function () {
    mainWindow = null;
  });
}

app.on('ready', createWindow);

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', function () {
  if (mainWindow === null) {
    createWindow();
  }
});

最后,执行:

npm install && npm start

运行结果如下图:

clipboard.png

当程序启动时,Electron 调用在 package.json 中定义的 main.js 文件并执行它。这个过程中,Electron 会创建一个主进程,主进程调用 BrowserWindow 模块创建浏览器窗口,每个浏览器窗口都有自己独立的渲染进程,渲染进程负责渲染 HTML 文件,以作为程序的 GUI 界面。

主进程管理所有页面和与之对应的渲染进程。每个渲染进程都是相互独立的,并且只关心他们自己的网页。

clipboard.png

至此,相信你对 Electron 的运行过程已有一定了解了,下面,我将介绍一下我是如何将我们的前端工作流程(tmt-workflow) 封装成桌面应用(WeFlow)的。

应用实践

现状

tmt-workflow : 是一个基于 Gulp(v4.0),通过约定一定的项目结构和配置文件实现高效、跨平台(Mac & Win)、可定制的前端工作流程。

其拥有 4 个任务(gulp task) :

  1. 开发任务(gulp build_dev)

  2. 生产任务(gulp build_dist)

  3. 部署任务(gulp ftp)

  4. 打包任务(gulp zip)

运行时需要先安装(npm install) ,再执行相应任务命令,也可以配合 WebStorm 等编辑器的 gulp 任务管理器 使用。

目标

利用现有的 tmt-workflow, 包装成一个 可视化 界面,不需要安装(npm install) ,直接下载打开即可使用。具体拥有:

  • 可视化的项目管理(新建、打开、配置、删除)

  • 可视化的全局项目配置

  • 可视化的任务执行(开发、生产编译、FTP 部署、Zip 打包)

  • 可视化的 log 日志反馈

设计效果预览

clipboard.png

主要由几部分组成:

  1. 第一次打开时的欢迎页

  2. 主窗体,由项目列表和任务列表组成,选择具体项目执行任务流程

  3. 全局设置页

  4. 项目设置页

  5. 关于

实现

核心: 如何将 gulp 程序转换

我们知道,gulp 的任务执行必需在命令行下执行,如: gulp build_dist ,这里的 gulp 是一个命令,是一个全局的 cli。执行时依赖于项目下的 node_modules

基于 gulp 程序的以上特点,我们的思路如下:

思路 1: 如果我们什么都不改变的话,直接把 tmt-workflow 这个 gulp 工作流封装,那可能的思路就是:

当点击可视化的任务按钮执行时,

  1. 先进入所要执行的项目的目录

  2. 再调用子进程执行 gulp 命令:

let exec = require('child_process').exec;
exec('gulp build_dist', {'cwd': 'projectPath'});

这样子,任何 gulp 流程都不需要改动,直接在其上面套一个壳,这个壳提供一下可视化的交互,然后帮你执行相应的 gulp 任务。

思路貌似挺好的,但跟我们的目标有点冲突,我们之所以要封装打包,为的就是省去用户安装,让用户打开即能用。而这个思路的执行方式需要在用户的项目目录下面执行 gulp 任务,那程序依赖的依然是用户已安装的 node_modules,而安装的过程有些模块(如图形模块)需要本地编译,而编译又依赖于用户系统的 node 版本和相关环境(如 win 下需要 python2.7.3 和 VS2010),这有时候是一个漫长又痛苦的过程。这就是为什么要省去安装的原因了。

所以,我们有了思路 2。

思路 2: 将 gulp 工作流程序node_modules 一起打包进 Electron ,当点击可视化的任务按钮执行时:

  1. 获取项目的路径

  2. 将整个项目传进 Electron 里面打包的工作流执行一遍

  3. 将编译后的文件输出

观察我们的 gulp 任务写法,都有一个固定的结构,如下:

//编译 less
function compileLess() {
    gulp.src(paths.src.less)
        .pipe(less())
        .pipe(gulp.dest(paths.dist.css))
}

//注册 build_dist 任务
gulp.task('build_dist', gulp.series(
    delDist,
    compileLess,
    ...
));

就是利用 gulp.src 读取资源,然后经过一系列处理之后再用 gulp.dest 输出。然后再通过 gulp 注册一个 gulp 任务,即可用 gulp 命令调用执行。如果可以把 gulp 从这个过程中去掉,换成普通的程序,则就可以不需要命令行调用,也就可以依赖于当前 Electron 打包的 node_modules ,实现封装的目的。

通过观察 gulp 的实现我们可以看到如下代码:

var vfs = require('vinyl-fs');

function Gulp() {
  Undertaker.call(this);

  // Bind the functions for destructuring
  this.watch = this.watch.bind(this);
  this.task = this.task.bind(this);
  this.series = this.series.bind(this);
  this.parallel = this.parallel.bind(this);
  this.registry = this.registry.bind(this);
  this.tree = this.tree.bind(this);
  this.lastRun = this.lastRun.bind(this);
}

Gulp.prototype.src = vfs.src;
Gulp.prototype.dest = vfs.dest;

我们发现,gulp.src 和 gulp.dest 实际上是 vinyl-fs 模块的实现。而原来 gulp 任务注册的 同步(gulp.parallel) 和 异步(gulp.series) 处理,我们也可以直接用 async 来替代,因此,我们稍微改动可以变成:

const async = require('async');
const vfs = require('vinyl-fs');

//编译 less
function compileLess(cb) {
    vfs.src(paths.src.less)
        .pipe(less())
        .pipe(vfs.dest(paths.dist.css))
        .on('end', cb);
}

async.series([
    function (next) {
        compileLess(next);
    }
], function (error) {
    if (error) {
        throw new Error(error);
    }
});

这个样子,就跟 gulp 无关了,但相关编译模块都还直接用的原来基于 gulp 的模块,所以,只需要稍加改动,就可以利用现有的 gulp 工作流快速实现 GUI 程序。

解决了核心的 gulp 流程转换,剩下的就是一些逻辑交互处理、配置功能、数据存储、菜单栏和快捷键功能等的实现了。下面对整个项目的相关实现进行介绍。

项目结构
WeFlow/
    ├── about.html              //关于界面
    ├── app.html                //主界面
    ├── assets/                 //资源目录
    │   ├── css
    │   ├── img
    │   └── js
    ├── main.js                 //应用入口文件
    ├── package.json
    ├── src/                    //源文件目录
    │   ├── _tasks/
    │   ├── app.js
    │   ├── common.js
    │   ├── createDev.js
    │   └── menu.js
    ├── templates/              //模版目录
    │   └── project.zip
    └── weflow.config.json      //配置文件
数据存储

WeFlow 需要对用户的一些操作进行记录(新建或打开了多少项目)进行存储,以便下次打开时还原。
Weflow 是一个本地程序,故数据不需要存储在云端,只需要存储在用户本地即可。所以直接使用 localStorage 来存储数据,WeFlow 构造的数据对象如下:

{
    "name": "WeFlow",
    "workspace": "/Users/littledu/WeFlow_workspace",
    "projects": {
        "project": {
            "path": "/Users/littledu/WeFlow_workspace/project",
            "devPath": "/Users/littledu/WeFlow/src/_tasks/tmp_dev/0c0876c4232f1de240f519f0920f2d60.js",
            "pid": 0
        }
    }
}

整个程序运行的过程中都是基于此对象进行操作。打开程序时,会读取此数据,进行界面内容填充。当项目位置或开发状态变动时,也更新数据存储进 localStorage。

菜单栏和快捷键功能设计

图片描述

menu 模块是一个主进程模块,可以用来创建原生菜单,每个菜单有一个或几个菜单项 menu items,并且每个菜单项可以有子菜单。

Electron 有一个 global-shortcut 模块专门用来设置(注册/注销)各种自定义操作的快捷键。但通过 menu 模块也可以绑定快捷键,代码如下:

const electron = require('electron');
const remote = electron.remote;
const Menu = remote.Menu;

var template = [
    {
        label: '文件',
        submenu: [
            {
                label: '新建项目',
                accelerator: 'CmdOrCtrl+N',
                click: function (item, focusedWindow) {
                    newProjectFn();
                }
            },
            {
                label: '打开项目…',
                accelerator: 'CmdOrCtrl+O',
                click: function (item, focusedWindow) {
                    let projectPath = remote.dialog.showOpenDialog({ properties: [ 'openDirectory' ]});
                    if(projectPath && projectPath.length){
                        openProject(projectPath[0]);
                    }
                }
            }
        ]
    }
];

menu 是主进程模块,但在这里想给快捷键绑定渲染进程中的功能。故调用了 remote 模块进行渲染进程和主进程通信。

遇到的问题

1. 浏览器自动刷新监听功能无法中断(browser-sync@2.13.0 之前)

tmt-workflow 使用 browser-sync 实现开发任务的自动刷新功能。常规情况下使用结束时,通过 cmd+c 或 ctrl+c 中断。然而封装后不再是通过命令行方式调用,故无法通过命令行来中断。 browser-sync 也没有提供 API 中断。故 WeFlow 中的 开发任务 跟其他的任务不同,解决方式是:

用子进程 child_process.fork 来执行开发任务的 dev.js,将返回的 PID 保存,即可通过这个 PID 来中断对应的子进程,达到停止开发任务的目的。

原理代码如下:

let childProcess = require('child-process');
function runDevTask(devPath){
    let child = childProcess.fork(devPath, {silent: true});

    child.stdout.on('data', function (data) {
        logReply(data.toString());
    });

    child.stderr.on('data', function (data) {
        logReply(data.toString());
    });

    child.on('close', function (code) {
        if (code !== 0) {
            logReply(`child process exited with code ${code}`);
        }
    });
}

function killChildProcess(pid){
    try {
        if(process.platform === 'win32'){
            childProcess.exec('taskkill /pid ' + pid);
        }else{
            process.kill(pid);
        }
    }
}

2. windows 下打包 EXE 后不能使用 process.stdout

官方认为,Electron 实现的都是 GUI 程序,所以理论上不需要这种输出功能。虽然在调试阶段并不影响,但打包的时候记得去掉,要不然会报错。

打包

electron-packager 可以用来打包 Electron 应用。生成各个平台的最终可运行文件,如 .app.exe

使用命令:

electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]
  • <sourcedir>: 项目的位置

  • <appname>: 应用名

  • --platform=<platform>: 打包的系统(darwin、win32、linux)

  • --arch=<arch>: 系统位数(ia32、x64)

  • --icon=<icon>: 指定应用的图标(Mac 为 .icns 文件,Windows 为 .ico 或 .png)

  • --out <out>: 指定输出的目录

  • --version=<version>: 指定编译的 electron-prebuilt 版本

例子:

electron-packager ./ WeFlow --platform=darwin --arch=x64 --icon=./assets/img/WeFlow.icns --overwrite --out ./dist --version=0.37.8

我们可以直接在 package.jsonscript 字段中添加脚本,如下:

"scripts": {
    "build:all": "electron-packager . --all --overwrite",
    "build:mac": "electron-packager ./ WeFlow --platform=darwin --arch=x64 --icon=./assets/img/WeFlow.icns --overwrite --out ./dist --version=0.37.8",
    "build:win64": "electron-packager ./ WeFlow --platform=win32 --arch=x64 --icon=./assets/img/WeFlow.png --overwrite --out ./dist --version=0.37.8",
    "build:win32": "electron-packager ./ WeFlow --platform=win32 --arch=ia32 --icon=./assets/img/WeFlow.png --overwrite --out ./dist --version=0.37.8 --app-version=1.0.0"
}

注意:不要认为一个系统可以完成所有系统的打包

如果你引用了一些原生模块(如 lwip),它是必需根据目标系统编译生成 .node 文件。遇到这种情况,则无法在一个系统上面打包另一个系统的可执行程序。更好的做法是利用 AppVeyorTravis 来为各平台实现打包自动化。可以通过相应官网进行了解。

electron-packager 打包后的文件可以看到源代码,想更进一步打包可以用 electron-builder

下载体验地址

参考文档

查看原文

赞 0 收藏 0 评论 0

littledu 回答了问题 · 2016-06-15

解决关于iscroll.js滚动容器中动态插入的元素无法滚动的问题,该如何解决?

这种情况是因为,初始化iscroll时,scroller高度并不是固定的。你可以在加载完你的dom后初始化iscroll。比如把iscroll的初始化方法写在ajax的success函数里面。

关注 5 回答 4

认证与成就

  • 获得 61 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-03-15
个人主页被 1.4k 人浏览