beccaFu

beccaFu 查看完整档案

杭州编辑浙江大学  |  没有专业 编辑  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

beccaFu 收藏了文章 · 2018-03-20

想要清晰的明白(二)CSS 盒模型Block box与Line box

在上一篇想要清晰的明白(一): CSS视觉格式化模型|盒模型|定位方案|BFC比较宏观的了解了盒子模型的作用,接下来就详细的介绍两种盒子的具体细节

Block Box

图片描述
(图自《css权威指南》)

display : block 、 list-item 以及 table 会让一个元素成为块级元素。
 在Block Box中,会被水平格式化,垂直格式化,那我们就分垂直和水平来讲讲

水平格式化

如何计算其宽度

正常流中,块级元素框的水平部分 = 其父元素的width = margin-left+margin-right + padding-left+padding-right+ border-left+border-right+自身width

这个理解非常重要!是等于其父元素的width!

width & margin & auto

clipboard.png

clipboard.png

  1. 为什么.a中设置padding为auto,均invalid?
    在padding-left/right,margin-left/right,border-left/right,width(我们简称下水平7大属性)中只有margin和width的值可能为auto

  2. 设置margin-left为10px,显示正确,为什么margin-right是剩余的所有宽度?
    因为当margin-left/right,width三个值均设置有固定宽度的时候,margin-right会根据包含块的width自动补齐

  3. 利用margin:0 auto 居中
    所以,利用这种方式居中的时候,必须是要设置居中元素的宽度,这样左右margin的值便会相等,从而引起的居中,这个和text-align:center只能块级元素的内联内容设置居中是不一样的。

垂直格式化

height与width一样,height定义了内容区的高度,而不是元素框的高度。元素框上下的内边距,边距,都会增加到height值里。

只有三个属性可以设置auto,height,和margin-top/bottom。注意!这里如果margin-top和margin-bottom同时设置为auto,也不会垂直居中,而是默认为零

垂直格式化,有一个很重要的方面是会造成垂直相邻外边距合并,解决这个的方式见想要清晰的明白(一)中的BFC部分。

负margin

水平方向

问: 水平7大属性相加要等于父元素的width,那margin负值会造成什么?

若width不是固定值,那么width的值则会增大
因为要满足条件等于父元素width,负margin相当于负值,width auto自动增大
若width为固定值,那么margin-right则会auto增大来满足这个条件

clipboard.png

灰色部分是body内的一个盒子,图二,没有定框使用负margin后,发生偏移,并且宽度增加,图三,定宽,发生偏移但是,宽度不增加,我们常常会发现出现莫名的水平滚动条,这里很有可能就是margin这小子在作祟

垂直方向

clipboard.png

黑色是接在灰色div后的一个div,可以看到,margin-bottom为负值,不会造成元素本身的移动,而是造成兄弟元素往上移动,就像我不动,拉了下面的人一把,而margin-top为负值,就像我们排成一队,然后像兔子跳一样一起往前面跳了一步。

从图二也可以看出来,黑色盒子对灰色盒子发生了覆盖,因为浏览器总是按从前到后的顺序显示元素,所以文档中后出现的正常流元素可能会覆盖较早出现的元素。

可以发现,灰色盒子的高度依旧保持着并且渲染出来了,但是CSS读取的高度已经减小,下面的元素自然往上移动了

负margin与float应用 ——双飞翼


不知道为啥每次看到这名字,就....莫名的想笑,雅蠛蝶


clipboard.png

    <div class="main">
      <div class="main-container">身子</div>
    </div>
    <div class="left">左边小翅膀</div>
    <div class="right">右边小翅膀</div>

分析

margin负值导致两个翅膀是覆盖在div.main上,我们看到的蓝色部分是.main-container,margin-left:-100%,相当于是把第二排的第一个,从后面拉动一个100%距离,也就是到了第一排和第一排第一个一起站着,第二排第一个走了,第二排第二个(也就是div.right)自动补到他的位置上,这时候又来了个margin-left:-200px利用到第二排第二个上,往前拉动200px的距离,于是就到了现在div.right的位置

总而言之,水平上的七个元素的宽度和一定会等于父元素的宽度

Line Box

每一行称为一条Line Box,它又是由这一行的许多inline-box组成,它的高度可以直接由line-height决定,line boxes的高度垂直堆叠形成了containing box的高度,就是我们见到的div或是p标签之类的高度了。

基础概念

匿名文本

<div>当你只有一把锤子<span>一切看起来</span>都像是颗钉子</div>
未包含在行内元素的字符串(当你只有一把锤子,都像颗钉子)就叫匿名文本

内容区 行内框 间距

clipboard.png

内容区
css假设每个元素都会生成一个或者多个Box,称为元素框,元素框中心有内容区,内容区外周围包括了padding,border,margin,但是,替换元素是包括外边距,内边距,边框的。
行间距
行间距是font-size与line-height的差值,被分成两半在内容区的上下
行内框
非替换元素,行内框高度=line-height
替换元素,行内框高度=内容区宽度(行间距不应用到替换元素)
行高
两行文字基线的距离
行框
一行有很多行内框,行框是包含这一行行内框最高点和最低点的
基线
不同元素的基线位置不同,整个行框会有一个基线,行内元素的位置是基于两者基线对齐

vertical-align(垂直对齐)

该属性 定义 行内元素的基线相对于该元素所在行的基线的垂直对齐的方式。
只有一个元素属于inline或是inline-block(table-cell也可以理解为inline-block水平)水平,其身上的vertical-align属性才会起作用.
同时也可以知道,改变其,会影响到行内框的位置,从而会影响到一整行行内元素的位置

需要注意vertical-align为数值时,会让文字上下移动,当其为百分比时是针对font-size的百分比

line-height(行高)

<div style="width:100px;height:10px"></div>
//这个div调整line-height不会发生变化,因为里面没有文字
<span style="line-height:10px;border:1px solid green"></span> 
//span的高度会随着line-height的变化而变化
//说明行内元素的高度是由line-height的支撑决定,行内框的高度也等于line-height

管理line-height

因为line-height是根据自己font-size设置,而不是父元素,所以将line-height设置为1em,该元素的line-height则会与相同(em单位是一般是相对与父元素进行设置大小)

<div style="font-size:12px;line-height:12px">
    <span style="font-size:15px;line-height:1em">
        嘿嘿嘿,这里的line-height值为15px
    </span>
</div>

margin padding border对行高的影响

  • 行内元素其padding、margin、border-top、border-bottom 不会增加行高。
    padding会覆盖;margin将重置为0;border-top和border-bottom同样会覆盖。

clipboard.png

  • css2.1规定margin-top和margin-bottom可以运用到不是行内非替换元素的所有其他元素

  • 行内替换元素(如:img)元素会影响行高

inline-block

将对象呈现为inline对象,但是对象的内容作为block对象呈现。之后的内联对象会被排列在同一行内。比如我们可以给一个link(a元素)inline-block属性值,使其既具有block的宽度高度特性又具有inline的同行特性。

参考资料

《CSS 权威指南 第三版》
《The Definitive Guide to Using Negative Margins》
margin为负值产生的影响和常见布局应用
CSS布局 -- 圣杯布局 & 双飞翼布局
CSS深入理解vertical-align和line-height的基友关系
深入理解CSS中的行高

查看原文

beccaFu 收藏了文章 · 2018-02-11

nodejs 中的 NODE_PATH

在使用 nodejs 开发中我们都免不了要去安装一些第三方模块。

那么你或多或少的遇到过以下一些问题

再继续阅读之前,我们先来弄清楚一个概念。

npm install --global xxx 属于全局安装
npm install xxx 属于本地安装

安装的模块太多项目太臃肿,模块没能复用

你写一个项目 A 需要安装一个 express 模块,又开发一个项目 B 又需要安装一个 express 模块

项目中依赖包太多,文件过多,模块没法复用,各种问题浮现

由于 安装的依赖包过多(而且依赖包嵌套依赖包),如果一个项目依赖多的话,(比如依赖 gulp 系列 或 grunt 系列的项目构模块)那么一个项目可以说轻轻松松上百兆。如果想给想项目更换一个目录,都发现是痴心妄想了。(基本都是1-10KB的小文本文件组成了一个100多MB的项目,那得有多少个文件啊!想想如果像java那样,模块都是以jar包存在的压缩归档文件可能也好一点)。更别提部署了。

其实这里面的代码也就是 几十KB到1兆 是我们自己写的代码。这些文件想实现 复制,移动,部署是很方面的。

给部署带来的困扰

如果你部署过 node 项目到远程服务器,node_modules 目录的上传将是一件恐怖的事情

NODE_PATH 出现,模块复用,最佳实践方案

NODE_PATH 是干什么的呢?
操作系统中都会有一个PATH环境变量,想必大家都知道,当系统调用一个命令的时候,就会在PATH变量中注册的路径中寻找,如果注册的路径中有就调用,否则就提示命令没找到。

bash-> export PATH=$PATH: # 将 /usr/bin 追加到 PATH 变量中
-> export NODE_PATH="/usr/lib/node_modules;/usr/local/lib/node_modules" #指定 NODE_PATH 变量

NODE_PATH 就是NODE中用来寻找模块所提供的路径注册环境变量。我们可以使用上面的方法指定NODE_PATH环境变量。并且用;分割多个不同的目录。

加载时机

关于 node 的包加载机制我就不在这里废话了。NODE_PATH中的路径被遍历是发生在
从项目的根位置递归搜寻 node_modules 目录,直到文件系统根目录的 node_modules,如果还没有查找到指定模块的话,就会去 NODE_PATH中注册的路径中查找

解决问题

基于 nodejs 的包加载路径搜索算法,我们可以 采用全局安装的方式,将我们的包安装到全局。
这样,我们的项目就可以共享全局中的依赖包。

了解全局

npm root -g 查看在你的系统中全局的路径。
我们也可以通过
npm config ls -l | grep prefix (*nix) 系统中
或是
npm config get prefix
来查看全局路径。
是的 prefix 字段就是全局base path

怎么设置全局路径呢?

# in *nix
npm config set prefix /path/to/global

# in windows
npm config set prefix C:\\Users\\pc\\global

求同存异,解决模块版本问题

差异性的解决方法

如果 项目A 使用了,express的3.x版本,项目B 使用了 express的4.x版本,那这种情况该怎么办呢?

可以将 NODE_PATH 指定的位置中存放 express的4.x版本,再将 项目B的 node_modules 目录中放置 3.x 版本。

这样就解决了模块版本差异性问题。

所以说,两种安装方式我们并不是只是用其中的一种,他们可以结合使用,根据 nodejs 的包加载机制,我们可以灵活使用。

部署不再是问题

在部署之前,我们可以将我们项目的所有可以全局安装的模块,都以全局的安装方式安装到服务器中。接下来我们就可以轻松,上传我们的项目到服务器中了。这样上传也会变得的很快。

然后配置我们的 NODE_PATH 环境变量。怎么配置上面也谈过,这里就不用多说了。(因为项目的部署方式多种多样,所以具体情况可以自行决定。)

本人是使用 PM2 部署管理Nodejs项目,所以我写在 配置文件中。

带来的问题

是的这种方式也有缺点。因为在使用 --global 参数的时候 --save--save-dev参数是无效的。
这样就带来一个问题。此时 package.json 中的 dependencies, devDependencies 将无法享受到npm自动更新带来的便利,不使用 dependencies, devDependencies 字段对我们的项目管理来说是不可接受的。

如何解决

我有一个不是很优雅地解决方法,但是也算是解决了这个问题,希望有更好解决办法的同学给我留言。
我写了一个小工具(npmafter),它的使用方法很简单, 它是跨平台的。兼容(Mac,Windows,Linux)。(我没有发布到github上,因为感觉会有更好的办法)

$ npm install -g http://yinchangsheng-blog.qiniudn.com/blog/nodejs/npmafter.tgz # install
# 然后我们安装任何模块就可以这样
$ npm install express -g | npmafter
$ npm install request q -g | npmafter --save
$ npm install mocha chai -g | npmafter --save-dev

是的 package.json 文件就会得到更新。
好的,如果你不纠结这个问题那么这个问题就算是解决了。

使用 NODE_PATH 可以很好的解决项目开发部署的问题。

查看原文

beccaFu 关注了用户 · 2017-10-12

villainhr @villainhr

现从事 微信社区核心开发
http://developers.weixin.qq.com

欢迎关注俺们的公众号:零度的田

关注 1060

beccaFu 发布了文章 · 2017-08-03

ThreeJS中的点击与交互——Raycaster的用法

基础概念

坐标系

我们的手机屏幕是二维的,但是我们展示物体的世界是三维的,当我们在构建一个物体的时候我们是以一个三维世界既是世界坐标来构建,而转化为屏幕坐标展示在我们眼前,则需要经历多道矩阵变化,中间webGL替我们操作了许多事情。

clipboard.png

  • 世界坐标系:在webGL中,世界坐标系是以屏幕中心为原点(0, 0, 0),且是始终不变的。你面对屏幕,你的右边是x正轴,上面是y正轴,屏幕指向你的为z正轴。长度单位这样来定:窗口范围按此单位恰好是(-1,-1,-1)到(1,1,1)。
  • 屏幕坐标系:

webGL的重要功能之一就是将三维的世界坐标经过变换、投影等计算,最终算出它在显示设备上对应的位置,这个位置就称为设备坐标。在屏幕、打印机等设备上的坐标是二维坐标。

  • 视点坐标系:

是以视点(照相机)为原点,以视线的方向为Z+轴正方向的坐标系中的方向。webGL会将世界坐标先变换到视点坐标,然后进行裁剪,只有在视线范围(视见体)之内的场景才会进入下一阶段的计算。

Raycaster

Raycaster threeJs官方文档

这个类设计用于鼠标去获取在3D世界被鼠标选中的一些物体


Raycaster( origin, direction, near, far ) 

origin — 射线的起点向量。
direction — 射线的方向向量,应该归一标准化。
near — 所有返回的结果应该比 near 远。Near不能为负,默认值为0。
far — 所有返回的结果应该比 far 近。Far 不能小于 near,默认值为无穷大。



找到点击物体的大致思路

clipboard.png

鼠标在屏幕上点击的时候,得到二维坐标p(x, y),再加上深度坐标的范围(0, 1), 就可以形成两个三位坐标A(x1, y1, 0), B(x2, y, 1), 由于它们的Z轴坐标是0和1,则转变到投影坐标系的话,一定分别是前剪切平面上的点和后剪切平面上的点,也就是说,在投影坐标系中,A点一定在能看见的所有模型的最前面,B点一定在能看见的所有的模型的最后边,将AB点连成线,AB线穿过的物体就是被点击的物体。而 Three.js提供一个射线类Raycasting来拾取场景里面的物体。更方便的使用鼠标来操作3D场景。(不过在实际代码中我们组成射线的两个点是摄像机所在视点与屏幕上点击的点连接而成的射线)

来一个Raycasting的官方实例

代码实现

function onDocumentMouseDown(e) {
    e.preventDefault();
    
    //将鼠标点击位置的屏幕坐标转成threejs中的标准坐标,具体解释见代码释义
    mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
    //新建一个三维单位向量 假设z方向就是0.5
    //根据照相机,把这个向量转换到视点坐标系
      var vector = new THREE.Vector3(mouse.x, mouse.y,0.5).unproject(camera);

    //在视点坐标系中形成射线,射线的起点向量是照相机, 射线的方向向量是照相机到点击的点,这个向量应该归一标准化。
    var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());

    //射线和模型求交,选中一系列直线
    var intersects = raycaster.intersectObjects(objects);
    console.log('imtersrcts=' + intersects)

    if (intersects.length > 0) {
        //选中第一个射线相交的物体
        SELECTED = intersects[0].object;
        var intersected = intersects[0].object;
        console.log(intersects[0].object)
    }


}

代码释义

clipboard.png

  //得到
 mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
 mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
  
 推导过程:
 设A点为点击点(x1,y1),x1=e.clintX, y1=e.clientY
 设A点在世界坐标中的坐标值为B(x2,y2);
 
 由于A点的坐标值的原点是以屏幕左上角为(0,0);
 我们可以计算可得以屏幕中心为原点的B'值
 x2' = x1 - innerWidth/2
 y2' = innerHeight/2 - y1
 又由于在世界坐标的范围是[-1,1],要得到正确的B值我们必须要将坐标标准化
 x2 = (x1 -innerWidth/2)/(innerwidth/2) = (x1/innerWidth)*2-1
 同理得 y2 = -(y1/innerHeight)*2 +1

参考资料

Three.js中的拾取
OpenGL中各种坐标系的理解
threejs对象拾取
《计算机图形学》
前端填坑指南

查看原文

赞 7 收藏 8 评论 7

beccaFu 回答了问题 · 2017-07-27

有没有可以用javascript做类似ACM算法题的平台或渠道,最好是国内的?

https://www.nowcoder.com/pat 不如来练PAT 可以直接用nodeJS

关注 7 回答 3

beccaFu 回答了问题 · 2017-07-27

前端研发想在windows电脑上安装公司的app,以方便调试,用什么工具?

用过 genymotion模拟器,和海马玩 都还不错啊~如果是调试app里面的网页,直接真机用vconsole太方便了

关注 7 回答 4

beccaFu 发布了文章 · 2017-04-29

从3dMax导出供threeJS使用的带动作模型与加载(认真修改详尽版)

评论区发现的建议,最近没空测试,先贴这

clipboard.png

还有好多人说找不到插件的 https://pan.baidu.com/s/1Q5g0... 密码:b43e 。 应该是他们现在只是维护blender,只有这个的插件,不如改用blender?

在自己做的一个小玩意中,发现要从3dMax中导出js文件供给threeJS使用,真是太多坑了!所以打算详细记录一下方法,好像开发会3dMax的比较少,但是至少可以帮助开发与美工更好的沟通与交流。在文末,我会附上一个可加载的js模型,方便学习~

图片描述

导出文件类型选型

在《THREE.JS开发指南》2015年第一印第一版中提及的支持的模型主要有一下几种

clipboard.png

图中的确是一些常见格式,我们可以通过这个表格对常见格式做一些了解,但是我们从Three.js的官方实例的Loaders包中可以看到,发现他现在提供了各种加载器来支持文件加载,我们可以根据我们的需求来选择我们需要的加载方式

clipboard.png

与3dmax自带的导出选择相比较,可以导出.FBX,Collada,STL,OBJ和MTL等文件,然后使用对应的Loader进行加载。我们可以选择自己熟悉的工具,熟悉的格式进行模型的导出,但是也正是由于他的多样性,导致无从下手,在尝试了Blender导出动作模型,与DDS素材转换等等工具之后,最终决定采用,我较为熟悉3dMax为建模工具,来导出我最熟悉的js文件来完成模型的导出与加载

下面是我经过实验后对一些常见格式的记录与分析大致统计出的一些选型分析

clipboard.png

clipboard.png

如果不需要做那么多选型的话,直接导出js好了~各方面都比较合适。

如何导出动态模型的js文件

安装3dMax导出插件

clipboard.png

.OBJ导出来的仅仅是模型,.MTL导出来的仅仅是材质,那动作呢?js在Three.js中支持的动作导出,在3dmax,maya,blender中并没有直接导出js的方法,庆幸的是大牛已经给我们开发了越来越丰富的插件了。在utils中的export中包含了这些插件(文件目录.\three.js-master\three.js-master\utils\exporters\max),我们需要将这些插件直接复制到3dmax的plugin文件夹中,如果都是需要导出包含动作的,只需要安装ThreeJSAnimationExporter.ms这一个文件就够了,重启3dmax后打开就可以看到这个插件了。(如下图)

clipboard.png

如何获取一个可加载模型(以一个简单的圆柱体为例)

clipboard.pngclipboard.png

  1. 首先你的模型必须是一个整体,比如你的模型是熊猫吃竹子,如果你的熊猫和竹子要一块导出,那么你的熊猫和竹子必须是一个整体
  2. 贴图只能有一张,且贴图是png格式
  3. 给bone(骨骼)认真命名,js导出后你可能会发现骨骼有点偏移与错位,严格的命名有助于你在建模或者导出的时候进行修改,不过对开发来说这点会比较麻烦,这点也不是必须的。
  4. 模型的每一个点都必须赋予权重,如果发生破面行为,那么非常有可能是加了动作的模型却有的店没有权重
  5. 骨骼测试可用的有bone和CS骨骼,如果采用其他骨骼最好进行测试
  6. 导出的时候你的模型必须是一个Mesh,非常重要!这就意味着你不能导出一个可编辑多边形或者其他形式,在我们添加skin修改器的时候,先添加一个EditMesh修改器在我们的物体上,如果你已经是一个editable mesh,那这就不需要了。最好再使用Unwrap, UVW,中途出现warning点确定就好。

clipboard.png

  1. 导出的时候选择模型就好,不要选择骨骼,如果把骨骼一起选择导出会出现如下错误

    clipboard.png

  2. 添加一个XForm修改器于你的模型当你开始导出之前,他会重置你物件的位置,转向等等,将它变为初始那样,最小化在导出时可能遇到的问题

    clipboard.png

  3. 如果发生错误,修复完毕后需要重新启动3dmax才能正常使用导出插件
  4. 可以先验证模型导出的数据量和点格式的正确与否再进行加载,一般导出文件的贴图地址会与你的地址不同,需要打开导出文件(如下图)修改贴图对应地址

    clipboard.pngclipboard.png

  5. 加载过程中你有可能看不见模型,那么很有可能是你的模型过大或者过小,可以直接在代码中用scale函数进行调整测试
  6. 如果还有问题,建议在重复以上几点查看一遍

代码中如何加载动态模型

在程序中主要是这几个步骤
clipboard.png

下面是代码的部分节选,其实官方的教程例子写的很完整,之后把一个完整可运行的实例传在GIt上~

/**
 * @method animateModel
 * @description 
 *  一个创建带动画模型的方法
 * @param {object} object.g 几何体 object.m 材质  object.name 名字
 */
var zz;
var animateModel = function(config) {
    var geometry = config.g;
    var materials = config.m;
    for (var i = 0; i < materials.length; i++) {
        var m = materials[i];
        m.skinning = true
    }

    // 创建材质,由于材质是多面材质,由材质数组组成故要调用MultiMaterial方法来创建一个新的材质
    var material = new THREE.MultiMaterial(materials);
    //创建出一个骨骼带蒙皮的网格对象
    var mesh = new THREE.SkinnedMesh(geometry, material);
    //给这个网格模型增加他的属性
    mesh.name = config.name;
    // 初始化模型位置
    mesh.position.set(0, 0, 0);
    mesh.geometry.computeVertexNormals();

    //用你的网格模型去创建一个骨骼帮助器 
    var skeletonHelper = new THREE.SkeletonHelper(mesh)
    skeletonHelper.material.linewidth = 3;
    // 我们可以打开skeletonHelper,这样可以看到骨骼,便于调整
    skeletonHelper.visible = true;
    //将骨骼添加到场景
    scene.add(skeletonHelper);

    // AnimationMixer 动画混合器 理解为这个动画各方面的一个管理者吧
    var mixer = new THREE.AnimationMixer(mesh);
    // 骨骼动画的动作片段保存在geometry中 下面是读取第一个动画的方式,所以animationFirst是一个AnimationClip
    var firstAnimation = geometry.animations[0];
    // AnimationAction是动作的schedule,之所以叫schedule是因为他可以控制着动画开始 结束 停止 这些流程
    var action = mixer.clipAction(firstAnimation);

    // 接下来可以为这个动画配置一些细节了
    action.clampWhenFinished = false;
//  0会停止,这里设置为0默认停止,不停要注意其他的地方是否有设置这个值,值越大越快
    action.setEffectiveTimeScale(0);
    action.play();
    mesh.mixer = mixer;
    mesh.action = action;
    mesh.skeletonHelper = skeletonHelper;
    return mesh;
}

//加载模型数据
var loader = new THREE.JSONLoader();
loader.load("static/img/model/czz3.js", function(geometry, materials) {
    var config = { name: "zz", g: geometry, m: materials };
                zz = animateModel(config);
                zz.status = 1;
                zz.action.time = 0;
                zz.action.setEffectiveTimeScale(0.7);
                zz.rotation.set(-26, 0, 0);
                zz.scale.set(7, 7, 7)
                zz.visible = true;
                scene.add(zz);
                addAnimateModel.animate();
                console.log(TWEEN.Tween)
                var zz_tween = new TWEEN.Tween(jichan.position).to({ z: 20 }, 10000)
                zz_tween.repeat(Infinity);
                zz_tween.start()
});
//主要理解render和animate这两个函数,模型能载入,缺动不起来主要都是这个袁术
var clock = new THREE.Clock();
function render() {
    // if (animation) animation.update(delta);
    var r = Date.now() * 0.0005;
    var delta = clock.getDelta();
    zz.mixer.update(delta);
    zz.skeletonHelper.update();
    stats.update();
    renderer.render(scene, camera);
}
function animate() {
    render();
    requestAnimationFrame(animate);
    trackBallControl.update();
}

静态模型的导出与加载

静态模型的导出,对我而言用3dmax直接导出obj最简单了,用导出OBJ和MTL格式,如果只要选择导出不带贴图的模型,那么在Material这一栏的Create mat-library则不用勾选.

require('OBJLoader.js')
require('MTLLoader.js')

/**
 * 加载一个有贴图模型
 */
const createMtlObj = argv => {

    THREE.Loader.Handlers.add(/\.dds$/i, new THREE.DDSLoader());
    //创建材质加载器
    let mtlLoader = new THREE.MTLLoader();
    // 设置材质加载路径 (相对路径)
    mtlLoader.setPath(argv.mtlPath);

    mtlLoader.load(argv.mtlFileName, function(materials) {
        materials.preload();
        let objLoader = new THREE.OBJLoader();
        objLoader.setMaterials(materials);
        objLoader.setPath(argv.objPath);
        objLoader.load(argv.objFileName, function(object) {
                // 加载模型完成后的回调函数
                if (typeof argv.completeCallback === 'function') {
                    argv.completeCallback(object);
                }
            },

            // 加载中
            function(xhr) {
                if (xhr.lengthComputable) {
                    var percentComplete = xhr.loaded / xhr.total * 100;
                    console.log(Math.round(percentComplete, 2) + '% downloaded');
                }
            },
            function(error) { console.log('error:' + error) }
        );
    });

}

一个js动态模型文件——一头不吓人的蜘蛛

太长了,链接: https://pan.baidu.com/s/1boI98hL 密码: ethr

查看原文

赞 16 收藏 28 评论 77

beccaFu 发布了文章 · 2017-04-29

A-Frame.js 学习&文档翻译(一)实体

A-Frame是什么

A-Frame是Mozilla 开源 web
虚拟现实框架,他能够非常方便的创建VR视口,载入部分格式的模型,设置照相机等,这为对计算机图形学不是很了解的同学,减轻了好多负担。我分别用了threeJS和A-Frame.js做了两个小项目,全英文文档看的好累,就顺便翻译了部分文档,之后会分享threeJS与模型导出与加载的一些坑。

A-Frame让你构建场景,仅仅通过HTML ,然而对你使用JavaScript,three.js,和其他的WebAPI没有限制,A-Frame使用一个采用的是一个实体-组件-系统的模式,使得他更加 的结构化,可延展,并且他不但是开源的,免费的,并且还是一个有着受欢迎的社区和完善工具与组件的生态系统。

Begin Example

clipboard.png

以我的webVR自主装修馆为例,如何构建出图中模型屋的场景?真的很简单

<head>
    <title>模板文件</title>
    <meta name="keywords" content="">
    <meta name="description" content="">
    <!--引入aframe.js  -->   
    <script data-original="static/js/bundle/aframe.js"></script>
</head>
<!--场景标签 他代表了整个场景的开始 是全局根物体,所有的实体(entity)都包含在场景里--> 
<a-scene>
  <!--<a-assets>使用资源管理系统来缓存资源,为了更好的性能,这个标签内的资源将会预加载缓存起来--> 
  <a-assets>
    <!--<a-asset-item>加载各种资源 比如3D模型或者材质  --> 
    <a-asset-item id="floor-obj" data-original="fox.obj"></a-asset-item>
    <!-- 加载图片  -->
    <img data-original="static/img/f1.jpg" id="f1-texture" alt=""> 
    <img data-original="static/img/sky_sphere.jpg" id='sky-box' alt="">
    <!--加载视频  --> 
    <video id="video" data-original="video.mp4"></video>
  </a-assets>
   <!--相机  --> 
  <a-camera fov="80"><a-cursor></a-cursor></a-camera>
  <!--materialchange__floor 组件名,material材质与属性细节 obj-model模型obj文件    -->
  <a-entity materialchange__floor material="src: #f1-texture;  metalness: 0.6;repeat:25;" id="floor" obj-model="obj: #floor-obj;"></a-entity>
  <a-video data-original="#video"></a-video>
  <!--天空盒  --> 
  <a-sky color="#EEEEFF" material="src: #sky-box"></a-sky>
</a-scene>

entity-component-system

A-Frame采用实体-组件-系统模式,使用这个框架的时候,我们把灯光,模型,材质等都当做是一个实体,就是我们呈现在视觉上的主要元素,而component,组件类似vue,封装可重用的代码,为项目添加功能,从而组装成一个系统

enrity 实体

A-Frame表现一个实体通过<a-entity>元素,作为定义在entity-component-system中的一个部分,这些实体是一个占位符,让我们来插入组件,并提供他的外观,行为和功能;

在A-Frame中,实体们的本质就是附加上位置,旋转和大小的组件

Example 例子

<a-entity geometry="primitive: box" material="color: red"
          light="type: point; intensity: 2.0" id="mario">
var el = document.querySelector('#mario');

如例所示,我们可以绑定组件给实体,让他渲染或者做些什么,我们可以通过几何(geometry)组件和材质(material)组件定义他形状与外观,可以使用灯光组件让他发出灯光;我们可以通过DOM API轻松的得到一个实体,一旦我们拿到了这个实体,我们就能去控制他一些详细的属性和方法。

Properties 属性

components

<a-entity>.components是附加在实体上的一个组件对象,这给我们一个途径去取得这个实体的组件,包括每一个组件的数据,状态和方法;

例如,如我我们要去取得threeJS里的照相机或者材质,我们可以这么去做:

var camera = document.querySelector('a-entity[camera]').components.camera.camera;
var material = document.querySelector('a-entity[material]').components.material.material;

或者一个组件所暴露的API

document.querySelector('a-entity[sound]').components.sound.pause();

isPlaying

实体是否处于active或者playing状态,如果我们pause这个实体,那么isPlaying将处于false状态

object3D

<a-entity>.object3D is a reference to the entity’s three.js Object3D representation. More specifically, object3D will be a THREE.Group object that may contain different types of THREE.Object3Ds such as cameras, meshes, lights, or sounds:

// Gaining access to the internal three.js scene graph.
var groupObject3D = document.querySelector('a-entity').object3D;
console.log(groupObject3D.parent);
console.log(groupObject3D.children);

我们可以获得不同类型的Object3Ds通过object3DMap,

object3DMap

一个实体的object3DMap 是一个对象,这个对象给了我们一个途径去获取不同类型的THREE.object3Ds(例如,相机,材质,灯光,声音)
例如一个绑定了几何和灯光组件的实体,他的object3DMap 应该是下面这个样子


{
  light: <THREE.Light Object>,
  mesh: <THREE.Mesh Object>
}

我们可以管理实体的THREE.Object3Ds通过getOrCreateObject3D, setObject3D, and removeObject3D.这些方法

sceneEl

一个实体有对其所属场景元素的引用

var sceneEl = document.querySelector('a-scene');
var entity = sceneEl.querySelector('a-entity');
console.log(entity.sceneEl === sceneEl);  // >> true.

Methods 方法

addState (stateName) 添加状态

addState将会给这个实体添加一个状态,这个方法可以触发stateadded 事件,并且我们可以检查到是否我们处于这个状态内

entity.addEventListener('stateadded', function (evt) {
  if (evt.detail.state === 'selected') {
    console.log('Entity now selected!');
  }
});
entity.addState('selected');
entity.is('selected');  // >> true

emit (name, detail, bubbles) 触发事件

emit触发一个在实体上定制的DOM事件,例如,我们能够触发一个事件去触发一个动画。

// <a-entity>
//   <a-animation attribute="rotation" begin="rotate" to="0 360 0"></a-animation>
// </a-entity>
entity.emit('rotate');

我们同样能够传递事件的细节或者数据通过第二个参数

entity.emit('collide', { target: collidingEntity });

这个事件是默认冒泡的,我们可以让他不要冒泡通过设置第三个参数为false

entity.emit('sink', null, false);

flushToDOM (recursive) 刷新DOM

flushToDOM 将会手动序列化一个实体组件的数据并且更新DOM,更多详细内容见 component-to-DOM serialization.

getAttribute (componentName) 获取属性

getAttribute 检索解析组件的属性,包含混入(mixin)的和默认的

// <a-entity geometry="primitive: box; width: 3">
entity.getAttribute('geometry');
// >> {primitive: "box", depth: 2, height: 2, translate: "0 0 0", width: 3, ...}
entity.getAttribute('geometry').primitive;
// >> "box"
entity.getAttribute('geometry').height;
// >> 2
entity.getAttribute('position');
// >> {x: 0, y: 0, z: 0}

如果组件名是一个没有注册的组件,getAttribute将会正常运行

// <a-entity data-position="0 1 1">
entity.getAttribute('data-position');
// >> "0 1 1"

getDOMAttribute (componentName)

getDOMAttribute只会检索解析显式定义在DOM的属性或者通过setAttribute设置的属性,如果componentName是已经注册的组件,getDOMAttribute将只会返回定义在HTML的组件数据,以一个数组对象的形式,他是不包括混合值和默认值的

与getAttribute的输出做一个比较:

// <a-entity geometry="primitive: box; width: 3">
entity.getDOMAttribute('geometry');
// >> { primitive: "box", width: 3 }
entity.getDOMAttribute('geometry').primitive;
// >> "box"
entity.getDOMAttribute('geometry').height;
// >> undefined
entity.getDOMAttribute('position');
// >> undefined

getObject3D (type)

getObject3D looks up a child THREE.Object3D referenced by type on object3DMap.

AFRAME.registerComponent('example-mesh', {
  init: function () {
    var el = this.el;
    el.getOrCreateObject3D('mesh', THREE.Mesh);
    el.getObject3D('mesh');  // Returns THREE.Mesh that was just created.
  }
});

getOrCreateObject3D (type, Constructor)

If the entity does not have a THREE.Object3D registered under type, getOrCreateObject3D will register an instantiated THREE.Object3D using the passed Constructor. If the entity does have an THREE.Object3D registered under type, getOrCreateObject3D will act as getObject3D:

AFRAME.registerComponent('example-geometry', {
  update: function () {
    var mesh = this.el.getOrCreateObject3D('mesh', THREE.Mesh);
    mesh.geometry = new THREE.Geometry();
  }
});

pause ()

pause()将会停止任何动画或者组件定义的动态行为,当我们停止一个实体的时候,它将会停止他的动画,并且调取这个实体上每个组件的Component.pause(),这个组件决定了发生什么当停止的时候,常常我们会移去事件监听,一个实体被pause后,他的子实体也会被pause()

// <a-entity id="spinning-jumping-ball">
entity.pause();

play ()

play()将会开始在动画或者组件中定义的动态行为,当DOM附加上一个实体的时候,这就好自动调用他,当一个实体play(),这个实体的子实体被调用play()

entity.pause();
entity.play();

例如,当播放声音的时候,这个声音组件会调用play()

setAttribute (attr, value, componentAttrValue)

如果属性不是已经注册的组件或者是单属性组件,我们将会这样来设置属性

entity.setAttribute('visible', false);

虽然attr是注册的组件的名字,它可能需要对值做特殊的解析,例如,这个位置组件是一个单属性组件,但是他的属性解析器允许他为一个对象

entity.setAttribute('position', { x: 1, y: 2, z: 3 });

设置多属性组件数据,我们可以将注册组件的名称作为attr传递,并将属性对象作为value传递:

entity.setAttribute('light', {
  type: 'spot',
  distance: 30,
  intensity: 2.0
});

Updating Multi-Property Component Data 更新多属性组件数据

为了更新多属性组件的单个属性,我们可以将注册组件的名称作为attr传递,属性名作为第二个参数,并且将属性值设置为第三个参数:

// All previous properties for the material component (besides the color)  will be unaffected.
entity.setAttribute('material', 'color', 'crimson');

setObject3D (type, obj)

setObject3D will register the passed obj, a THREE.Object3D, as type under the entity’s object3DMap. A-Frame adds obj as a child of the entity’s root object3D.

AFRAME.registerComponent('example-orthogonal-camera', {
update: function () {

this.el.setObject3D('camera', new THREE.OrthogonalCamera());

}
});

removeAttribute (attr, propertyName)

如果attr是注册组件的名称,除了要从DOM中删除属性,removeAttribute还将从实体中分离组件,调用组件的删除生命周期方法。

entity.removeAttribute('goemetry');  // Detach the geometry component.
entity.removeAttribute('sound');  // Detach the sound component.

如果给定propertyName,removeAttribute将重置propertyName指定的属性的属性值为属性的默认值:

entity.setAttribute('material', 'color', 'blue');  // The color is blue.
entity.removeAttribute('material', 'color');  // Reset the color to the default value, white.

removeObject3D (type)

removeObject3D removes the object specified by type from the entity’s THREE.Group and thus from the scene. This will update the entity’s object3DMap, setting the value of the type key to null. This is generally called from a component, often within the remove handler:

AFRAME.registerComponent('example-light', {
  update: function () {
    this.el.setObject3D('light', new THREE.Light());
    // Light is now part of the scene.
    // object3DMap.light is now a THREE.Light() object.
  },
  remove: function () {
    this.el.removeObject3D('light');
    // Light is now removed from the scene.
    // object3DMap.light is now null.
  }
});

removeState (stateName)

removeState将从实体中弹出一个状态。这将会触发stateremoved事件,我们可以检查它的删除状态。

entity.addEventListener('stateremoved', function (evt) {
  if (evt.detail.state === 'selected') {
    console.log('Entity no longer selected.');
  }
});
entity.addState('selected');
entity.is('selected');  // >> true
entity.removeState('selected');
entity.is('selected');  // >> false

Events 事件

clipboard.png

查看原文

赞 4 收藏 12 评论 10

beccaFu 赞了回答 · 2016-06-07

【官方比赛】社区 1111 秀代码,让你来秀让你飞!

// language: javascript
// 打开控制台运行,亲
Function("‍‍‌‍‍‌‍‍‍‍‌‍‌‍‍‍‍‍‌‍‍‍‌‍‍‍‌‍‌‌‌‍‍‌‌‌‍‌‌‌‍‌‌‍‌‍‍‌‍‌‌‍‍‌‍‍‍‌‌‍‍‌‌‌‍‌‌‍‍‌‍‌‍‌‌‌‍‌‍‍‍‍‌‍‌‌‍‌‍‌‌‌‍‌‌‍‍‌‌‍‌‌‌‌‍‌‌‌‍‌‍‍‍‌‌‍‍‌‍‌‍‍‌‍‍‍‍‍‍‍‌‍‌‌‌‍‍‌‌‍‌‌‍‍‍‌‌‍‌‍‍‌‍‌‌‍‌‍‌‌‍‌‌‍‍‌‍‌‍‌‍‌‌‍‌‌‍‌‌‍‍‌‍‍‍‌‌‍‍‍‍‌‍‌‌‌‍‌‍‍‍‌‌‍‍‍‍‌‍‍‌‍‌‌‍‌‍‌‌‍‌‍‍‌‍‌‌‍‍‌‍‍‍‍‌‌‌‌‍‌‍‌‍‌‌‌‍‍‍‍‌‍‍‍‌‍‍‍‌‌‍‍‍‌‍‍‌‌‍‍‍‍‍‍‌‌‍‍‌‍‍‍‌‌‍‍‍‍‍‍‌‌‍‍‍‍‍‍‌‌‍‍‍‍‍‍‌‌‍‍‍‍‍‍‌‌‍‍‍‍‍‍‌‌‍‍‍‍‍‍‌‌‍‍‍‍‍‍‌‌‍‌‌‌‍‍‌‌‍‌‌‍‍‍‌‌‍‌‌‌‍‍‌‌‍‌‌‍‍‍‌‌‍‍‌‍‍‍‌‌‍‍‍‌‍‌‍‌‌‌‍‍‍‍‌‍‍‍‌‍‍‌‍‌‌‌‍‌‍‍‌‍‍‍‌‍‍‍‌‍‌‍‍‌‍‍‌‍‌‌‌‍‍‌‌‍‍‍‌‌‍‌‌‍‌‌‍‍‍‌‌‍‌‍‍‌‍‌‌‍‍‍‌‌‍‌‌‍‌‍‌‌‍‍‌‍‌‍‍‍‍‍‌‍‌‍‍‌‍‍‌‍‌‌‍‍‍‌‌‍‍‍‌‌‍‌‌‍‌‌‌‌‍‌‌‍‌‌‌‍‍‌‌‌‍‍‌‌‍‌‌‍‌‌‌‌‍‌‌‍‌‌‍‍‍‌‌‍‍‌‍‌‍‍‌‍‌‌‌‍‍‌‌‍‌‌‍‍‍‌‌‍‌‌‌‌‍‌‌‍‍‌‌‌‍‍‌‍‌‍‍‍‍‍‌‍‍‍‌‍‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‍‌‍‌‍‍‌‌‌‍‍‌‍‍‌‌‍‌‌‌‍‍‌‌‍‍‌‌‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‍‌‌‌‍‍‌‌‌‍‍‌‍‍‌‌‍‌‍‌‍‌‌‍‍‌‍‌‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‌‍‍‍‍‌‌‍‍‌‌‍‍‌‌‍‍‌‍‍‍‍‌‌‌‍‍‍‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‍‌‌‍‍‍‌‌‍‌‌‍‍‍‌‌‍‍‌‍‍‌‌‍‍‌‌‍‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‌‍‍‍‍‍‌‌‌‍‍‌‍‍‌‌‌‍‍‍‍‍‌‌‍‍‍‌‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‌‍‍‍‍‌‌‍‍‌‌‍‍‌‌‍‍‌‌‍‍‌‌‍‍‌‍‍‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‍‌‌‌‍‍‌‌‍‌‌‍‍‍‌‌‌‍‍‍‍‍‌‌‍‌‍‍‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‌‌‍‍‌‌‍‍‌‌‍‍‌‌‍‍‍‌‌‍‍‍‍‍‌‌‍‍‍‌‌‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‍‌‍‍‍‌‌‍‍‌‍‌‍‍‌‌‍‍‍‍‍‍‌‌‍‌‌‌‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‍‌‍‍‍‌‌‍‍‌‍‌‍‍‌‌‍‍‍‍‍‍‌‌‍‍‍‍‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‍‌‍‌‍‍‌‌‌‍‍‌‍‍‌‌‍‌‌‌‍‍‌‌‌‍‍‌‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‍‌‌‌‍‍‌‌‍‌‌‌‍‍‌‌‌‍‍‍‍‌‌‍‍‌‍‌‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‍‌‍‍‍‌‌‍‍‌‍‌‍‍‌‌‌‍‍‍‍‍‌‌‍‌‌‍‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‍‌‌‍‌‍‌‍‍‌‌‍‌‍‍‍‍‌‌‍‌‌‍‍‍‌‌‍‍‌‍‍‌‍‌‌‌‍‍‍‌‌‌‍‌‍‌‍‌‌‍‍‌‌‍‍‌‌‍‍‌‌‍‍‍‌‌‍‍‍‍‍‌‌‍‍‍‌‌‍‍‌‍‍‍‌‍‍‍‌‍‌‍‍‌‍‍‌‌‌‍‌‌".replace(/.{8}/g,function(u){return String.fromCharCode(parseInt(u.replace(/\u200c/g,1).replace(/\u200d/g,0),2))}))();

关注 70 回答 48

beccaFu 赞了文章 · 2016-06-06

来!我们一起来从头开始构建自己的JavaScript模块化工具

前言

希望编写程序能像玩积木一样,首先规划要产出怎样的作品,然后在积木堆中挑选合适的积木块,最后一组合就完工了。

于是JavaScript需要类似这样模块化,每个模块都隐藏内部细节并且对外暴露接口,再处理好模块之间的依赖关系,就可以达到玩积木的效果了。

虽然现有很多模块化框架/工具,但对于新手来说,为什么不自己撸一个简单的模块化工具呢?

希望通过这个工具把自己觉得好用的代码以模块的方式组织起来,渐渐形成自己的JS库,之后可以勇敢地和HR说,自己的小项目用的是自己小JS库,^_^。我觉得,在这个封装的过程中,新手能学习到很多东西。

新手嘛,多造轮子总是有好处的,=_=。

从闭包到模块

以下是《你所不知道的JavaScript(上卷)》中对于闭包的说明。

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

其实,不管怎样,闭包正如其字面意思一样,既能提供一个相对封闭的空间,也能向外界暴露必要的接口。这不就正符合我们模块化的需求吗?

在此,建议参考这篇文章,以加强您对闭包的理解:《假如技术HR问您JavaScript的“闭包”,嘿嘿嘿,举这个例子就够了》

最简单的模块

var test = (function test(){
    function run(){
        console.log("run test");
    }
    return {
        run: run
    };
})();
test.run();

上面的代码有个叫test的函数作为模块创建器,每次调用它都可以创建一个新的模块。这里使用立即执行函数,立即创建了一个test模块。参考闭包的概念,外部可以调用test模块中的run函数,同时test模块又有自己独立的作用域。能达到一个积木块(模块)的要求。

简单的模块这样写没有问题,但是模块间的依赖问题没有解决。

最简单的模块管理工具

模块之间必然会存在依赖关系,而模块管理工具需要能够很好地管理模块间的依赖。下面我们模仿实现了AMD规范的工具requirejs,主要是模仿其define,get的API风格,自己写一个简单的版本。

//模块管理工具,MyModules
var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i=0; i<deps.length; i++){
            //将依赖的名字替换成已经注册了的模块
            deps[i] = modules[deps[i]];
        }
        //将依赖数组展开成参数传入模块的构建函数,生成新模块
        modules[name] = impl.apply(impl, deps);
    }
    function get(name){
        return modules[name];
    }
    return {
        define: define,
        get: get
    }
})();
//定义一个模块,data
MyModules.define("data",[],function(){
    var name = "miku";
    function getName(){
        return name;
    }
    return {
        getName:getName
    }
});
//定义一个模块,app
//该模块依赖data模块
MyModules.define("app", ["data"], function(data){
    function run(){
        console.log(data.getName());
    }
    return {
        run:run
    }
});
//取出app模块
var app = MyModules.get("app");
//调用app模块的run方法
app.run();

可以看出,利用MyModules可以很方便地定义使用模块,管理模块依赖。但是还存在一个问题,MyModules对于模块定义的顺序有要求。以上面的例子来说,就是app模块依赖data模块,那data模块必须在app模块之前被定义。这个限制让我们实际使用中不是很方便。接下来我们将改进它。

改进模块管理工具

我们需要让模块管理工具不需要限制模块的定义顺序,这里我的做法是,使用一个rebuilds数组来保存未成功构建的模块。每次有新模块构建成功的时候就会重新尝试去构建整个rebuilds数组中的模块。具体看下面的代码。

window.mm_modules = (function Manager() {
    var debug = false;
    var modules = {};
    var rebuilds = [];
    function copyArray (array){
        var tempArray = [];
        for(var i=0; i<array.length; i++){
            tempArray.push(array[i]);
        }
        return tempArray;
    }
    function define(name, deps, impl) {
        //判断依赖构建是否成功
        var depsDone = true;
        
        //拷贝一份当前想要构建的模块,当构建失败时使用
        var rebuildCopy = {
            name : name,
            deps : copyArray(deps),
            impl : impl
        };
        
        //循环依赖数组,构建依赖
        for (var i=0; i<deps.length; i++){
            //将依赖的名字替换成已经注册了的模块
            deps[i] = modules[deps[i]];
            //有依赖的模块未定义,所以这个模块构建失败
            if(!deps[i]){
                depsDone = false;
                break;
            }
        }
        
        //如果依赖构建成功,即模块构建成功
        if(depsDone){
            //将依赖数组展开成参数传入模块的构建函数,生成新模块
            modules[name] = impl.apply(impl, deps);
            //从rebuilds数组中移除
            if(rebuilds[name]){
               delete rebuilds[name];
            }
            //循环rebuilds数组,尝试从新构建之前构建失败的模块
            for (key in rebuilds){
                var rebuild = rebuilds[key];
                if(rebuild){
                    //递归调用
                    define(rebuild.name, rebuild.deps, rebuild.impl);
                }
            }
        }
        //模块构建失败,存入rebuilds数组中,等待下一次重新构建
        else{
            rebuilds[name] = rebuildCopy;
        }
        if(debug){
            console.log("[mm_modules debug]need rebuild modules:", rebuilds);
        }
    }
    function get(name){
        return modules[name];
    }
    return {
        define: define,
        get: get
    }
})();

改进后的模块管理工具,能够自动地处理模块依赖,而不需要限制定义顺序了。
那,能不能更进一步呢?试着想一下,我们日常会怎么使用?单文件单模块,然后把这些文件放在不同文件夹里组织好。于是,我就想到使用gulp这样的工具辅助我们。

gulp辅助

请参考下面的目录结构。

├── dist
│   ├── index.html
│   └── js
│       └── mm-modules-build.js
├── gulpfile.js
├── mm-modules
│   ├── queryObject.js
│   ├── request.js
│   ├── template.js
│   ├── test.js
│   └── util.js
├── mm-modules.js

可以在mm-modules下随意地定义模块,如util模块内有各种工具函数,template模块则包含了artTemplate模版引擎。之后利用gulp将mm-modules.js(模块管理工具)与mm-modules下所有的模块文件打包成mm-modules-build.js。项目中只要引入mm-modules-build.js即可。

结尾

到此,我们自己构建了一个很实用的JavaScript模块化工具,项目的源码在这里:https://github.com/MIKUScallion/mm-modules,喜欢的话,给个✨。

再回顾一下前言的话。

希望通过这个工具把自己觉得好用的代码以模块的方式组织起来,渐渐形成自己的JS库,之后可以勇敢地和HR说,自己的小项目用的是自己小JS库,^_^。我觉得,在这个封装的过程中,新手能学习到很多东西。

新手嘛,多造轮子总是有好处的,=_=。

参考

  • 《你所不知道的JavaScript(上卷)》

查看原文

赞 5 收藏 67 评论 2

认证与成就

  • 获得 65 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-02-13
个人主页被 1k 人浏览