精华内容
下载资源
问答
  • 作者:彭晓韬 日期:2020.04.28 [文章摘要]:薄膜干涉是一种日常生活中很常见的物理现象,对其干涉条件也有比较系统性研究。但就自然光(太阳光)条件下,往往大于一定厚度薄膜(也是为什么叫薄膜干涉而不叫膜干涉...

    作者:彭晓韬

    日期:2020.04.28

    [文章摘要]:薄膜干涉是一种日常生活中很常见的物理现象,对其干涉条件也有比较系统性的研究。但就自然光(太阳光)条件下,往往大于一定厚度的薄膜(也是为什么叫薄膜干涉而不叫膜干涉的原因所在)是不会出现干涉的,而厚度小到与可见光波长相近的肥皂泡,甚至接近分子直径的油膜却可以产生干涉现象。这就与光程差大于光的半波长即可产生干涉的条件相背。也与光是具有动能与动量的粒子假设相背:光子为什么不会穿透薄到与分子直径接近的油膜,反而会在分子表面与底面形成反射?本文就此提出一些不成熟的看法与想法,供有兴趣进一步研究者参考。

    一、薄膜干涉现象及机理简介

    52e9611dc8e54eaa444380114fa8ffe0.png
    0166bddeaa7c68ab1161f13191bb56c1.png

    从上图一和二可知:在太阳光照片下,空气中分布较均匀的微小水滴可使太阳光产生散射并形成类似薄膜干涉一样的彩虹。其机理是:水滴表面的散射光与入射进水滴并被水滴内表面反射出来的透射光合并并照射到人眼时,其复合后的光的主频会因入射进入水滴内部再被反射而透射出来的光程差主要与视角度有关而出现不同颜色的光带:波长与光程差相同或相近或成倍数关系的光将得到加强,其它波长的光将变弱。光程差小的复合后得到加强的光的频率偏高而呈现出蓝色光带,反之呈现出红色光带。视高度大于太阳视高度的水滴产生的光程差随视高度减小,因此,红光带在下方,蓝光带在上方;而视高度小于太阳视高度的水滴产生的光程差随视高度增大,因此红光带在上方,蓝光带在下方。这就是彩虹为什么会出现上下颜色颠倒的原因所在。

    59eb4cdf9bcf7cf9a8cae864a1f1c573.png
    c736177d83d7eaf93cba171d0ae657a8.gif

    图四 薄膜干涉动图

    上图三和四为薄膜干涉图像,其原理与彩虹完全相同:薄膜表面的散射光与进入薄膜并被薄膜另一侧面反射回来的透射光合并后进入人眼或照相机后就会因不同视角度的光程差不同而呈现出不同的颜色。当薄膜厚度基本相同时,则因光程差仅与视角度有关,因此,相同视角度上的复合光的颜色相同,从而随视角度不同形成光同颜色的光带。

    需要指出的是:同时刻的彩虹或薄膜干涉条纹会因观测者的位置不同而不同。即:站立在不同位置上的人看到的彩虹或薄膜干涉条纹的位置或高度是不尽相同的

    二、薄膜厚度对薄膜干涉现象的影响分析

    1、薄膜光程差的计算

    e27702cc5d17deea1fa4587b35c7f797.png

    由上图五可知:当不考虑经薄膜底部反射回来的光路的入射角与薄膜表面反射光路的入射角差异时,两条光路的光程差△S计算公式为:

    △S=2n1d/cosθ-2n0dtgθsinα (公式1)

    根据折射率与入射/折射角间的关系式:sinα/sinθ=n1/n0可得:

    Sinθ=n0sinα/n1

    代入(公式1)可得:

    beb058a73c09eb0023a511d905fea535.png

    为方便起见,可令n0=1,则(公式2)可简化为:

    4fa1c614444cba4bf8b6e76176ef00c1.png

    则有:△S=kd (公式4)

    由(公式4)可知:当入射与反射角α一定时,光程差仅与薄膜的厚度d成正比。也就是随着薄膜厚度的增加,光程差随之增加。

    7c9a1f652ca374992d2c8fcb633adaa5.png

    从上表一可知:

    A、光程差系数与折射率的关系是:随着薄膜材质的折射率的增加,光程差系数越来越大。

    B、光程差系数与入/反射角间的关系是:随着入/反射角度的增大,光程差系数由小逐渐变大,入/反射角度到22˚左右时达到最大;随后又逐渐变小,入/反射角度到55˚左右时达到最小值;随后又逐渐增大。光程差系数的最小值与最大值相差约1.2~1.8倍(材料的折射率越大,最大值与最小值差异越小),与可见光波长的最小与最大波长之比基本相同(380~780nm)。这就是在薄膜厚度相同情况下,干涉条纹会出现可见光所有颜色构成的相间亮条纹的原因所在。

    2、薄膜厚度对薄膜干涉现象的影响

    我们知道:干涉条纹的明暗(单色光)或条纹颜色(全色光)决定于光程差△S与光的波长λ的比值,当比值为整数时为明条纹或可见色,当比值为0.5、1.5、2.5、………时为暗条纹或不可见色。

    对于自然光而言,入射光的波长为可见光波段内的所有可能数值。而薄膜干涉能看到的都是波长与光程差比值为整数倍的光,其余波长的光会被削弱或相互抵消而不可见。当入射角与反射角不变时,薄膜厚度的增加将导致光程差随之增加,与之匹配的、可见(得到加强的)光的波长也随之增加,直到波长最长的红光不能产生干涉时,则会进入下一个循环:波长最短的紫色光成为亮条纹。

    三、光的相位稳定性和持续性对薄膜干涉现象的影响

    由光(电磁波)的产生机制可知:不同频率的光是由不同运动状态或运动频率的带电体产生的。从光的频率特性一般可分为三类:

    第一类为线性与非线性连续组合谱:原子外层电子在不同能级间跃迁期间,电子产生连续频率的光(单一频率的强度很小以致于在原子光谱分析中被忽视),而原子核产生单一频率的光。两种不同频率类型的光组成了组合谱;

    第二类为非线性的连续谱:分子/原子热运动期间,由原子核与外部电子构成的时变电偶极矩的电偶极子产生与其电偶极矩变化频率相同的光,但因任何物质的分子/原子热运动期间,不同分子/原子的热运动速度、方向、行程及频率(导致分子/原子成为时变电偶极子)均不相同,一般呈现类似正态分布(普朗克黑体辐射强度与频率关系公式与麦克斯韦气态物质分子速度分布率)。这种情况下产生的光为非线性的连续谱。光强度的峰值所对应的频率与发光体的温度正相关。

    第三类为人工产生的可调制谱:人工用LC振荡电路、压电石英等产生的固定频率(单一频率或线性谱)的谐波,并在谐波基础上进行振幅的调制而形成的调制谱。如电视和广播信号,手机信号等。

    就以上三类光(电磁波)的相位稳定性与持续性而言,一类光的相位稳定性与持续性最差,一般与电子跃迁持续时间相同,约为10-14秒级;二类光的相位稳定性和持续性次之,一般为纳秒级。其相位稳定性和持续性主要由分子/原子热运动状态稳定性决定;三类光的相位稳定性和持续性最高,如果不对人工产生的光进行调制,其相位的稳定性和持续性可以无限大。就如工业电的频率为50Hz的谐波,其相位基本上稳定不变且可持续。

    由以上分析可知:薄膜干涉使用的光的类型不同将决定可形成薄膜干涉现象的薄膜厚度的极大值。假设光的相位稳定性和持续性的最大值为△t,则当△S/C=Kd/C>△t,d>△tC/K时,将不能产生薄膜干涉现象。

    以自然(太阳)光为例,若相位稳定性最大值为△t=1ns,能产生薄膜干涉现象的最大薄膜厚度d=0.000001*C/K≈0.226~0.083m。实际上,太阳光下能形成薄膜干涉的薄膜厚度远小于计算值,因此,自然光的相位稳定性和持续性应该远小于1纳秒。

    四、薄膜干涉现象的物理意义

    1、薄膜干涉存在薄膜厚度上限证明光的相位存在稳定和持续性上限,也就证明了光不是粒子。否则,就不应该出现薄膜厚度的上限;

    2、薄膜干涉中薄膜厚度下限可为分子直径证明光不是携带动能与动量的粒子。否则,光粒子不可能在分子的上界面与下界面上发生反射(实际上是油膜分子和油膜下的水分子被入射/透射光激励成为了次生光源而产生的次生光)而不穿透薄膜;

    3、薄膜干涉具有频率的选择性证明光不是粒子。否则,自然光不可能因为反射角的不同而出现全色光变为不同的单色光,而其他颜色的光消失或不可见。

    总之,薄膜干涉现象证明光是变化的电场与磁场,而不是光粒子,更不具波粒二象性。

    展开全文
  • 所谓软件架构风格,是指描述某个特定应用领域中系统组织方式的惯用模式。架构风格定义一个词汇表和一组...没多少人能记住上面的定义,需要注意的是本文不是专业讨论系统架构的文章,笔者也还没到那个水平. 所以暂...

    所谓软件架构风格,是指描述某个特定应用领域中系统组织方式的惯用模式。架构风格定义一个词汇表和一组约束,词汇表中包含一些组件及连接器,约束则指出系统如何将构建和连接器组合起来。软件架构风格反映了领域中众多系统所共有的结构和语义特性,并指导如何将系统中的各个模块和子系统有机的结合为一个完整的系统

    没多少人能记住上面的定义,需要注意的是本文不是专业讨论系统架构的文章,笔者也还没到那个水平. 所以暂时没必要纠结于什么是架构模式、什么是架构风格。在这里尚且把它们都当成一个系统架构上的套路, 所谓的套路就是一些通用的、可复用的,用于应对某类问题的方式方法. 可以理解为类似“设计模式”的东西,只是解决问题的层次不一样

    透过现象看本质,本文将带你领略前端领域一些流行技术栈背后的架构思想。直接进入正题吧

    文章大纲

    • 分层风格

    • Virtual DOM

    • Taro

    • 管道和过滤器

    • 中间件(Middleware)

    • 事件驱动

    • MV*

    • 家喻户晓的MVC

    • Redux

    • 复制风格

    • 微内核架构

    • 微前端

    • 组件化架构

    • 其他

    • 扩展阅读

    分层风格

    没有什么问题是分层解决不了,如果解决不了, 就再加一层 —— 鲁迅

    不不,原话是: Any problem in computer science can be solved by anther layer of indirection.

    分层架构是最常见的软件架构,你要不知道用什么架构,或者不知道怎么解决问题,那就尝试加多一层。

    一个分层系统是按照层次来组织的,每一层为在其之上的层提供服务,并且使用在其之下的层所提供的服务. 分层通常可以解决什么问题

    • 是隔离业务复杂度与技术复杂度的利器. 典型的例子是网络协议, 越高层越面向人类,越底层越面向机器。一层一层往上,很多技术的细节都被隐藏了,比如我们使用HTTP时,不需要考虑TCP层的握手和包传输细节,TCP层不需要关心IP层的寻址和路由。

    • 分离关注点和复用。减少跨越多层的耦合, 当一层变动时不会影响到其他层。例如我们前端项目建议拆分逻辑层和视图层,一方面可以降低逻辑和视图之间的耦合,当视图层元素变动时可以尽量减少对逻辑层的影响;另外一个好处是, 当逻辑抽取出去后,可以被不同平台的视图复用。

    关注点分离之后,软件的结构会变得容易理解和开发, 每一层可以被复用, 容易被测试, 其他层的接口通过模拟解决. 但是分层架构,也不是全是优点,分层的抽象可能会丢失部分效率和灵活性, 比如编程语言就有'层次'(此例可能不太严谨),语言抽象的层次越高,一般运行效率可能会有所衰减:


    分层架构在软件领域的案例实在太多太多了,咱讲讲前端的一些'分层'案例:

    Virtual DOM

    前端石器时代,我们页面交互和渲染,是通过服务端渲染或者直接操作DOM实现的, 有点像C/C++这类系统编程语言手动操纵内存. 那时候JQuery很火:


    后来随着软硬件性能越来越好、Web应用也越来越复杂,前端开发者的生产力也要跟上,类似JQuery这种命令式的编程方式无疑是比较低效的. 尽管手动操作 DOM 可能可以达到更高的性能和灵活性,但是这样对大部分开发者来说太低效了,我们是可以接受牺牲一点性能换取更高的开发效率的.

    怎么解决,再加一层吧,后来React就搞了一层VirtualDOM。我们可以声明式、组合式地构建一颗对象树, 然后交由React将它映射到DOM:


    一开始VirtualDOM和DOM的关系比较暧昧,两者是耦合在一起的。后面有人想,我们有了VirtualDOM这个抽象层,那应该能多搞点别的,比如渲染到移动端原生组件、PDF、Canvas、终端UI等等。

    后来VirtualDOM进行了更彻底的分层,有着这个抽象层我们可以将VirtualDOM映射到更多类似应用场景:


    所以说 VirtualDOM 更大的意义在于开发方式的转变: 声明式、 数据驱动, 让开发者不需要关心 DOM 的操作细节(属性操作、事件绑定、DOM 节点变更),换句话说应用的开发方式变成了view=f(state), 这对生产力的解放是有很大推动作用的; 另外有了VirtualDOM这一层抽象层,使得多平台渲染成为可能。

    当然VirtualDOM或者React,不是唯一,也不是第一个这样的解决方案。其他前端框架,例如Vue、Angular基本都是这样一个发展历程。

    上面说了,分层不是银弹。我们通过ReactNative可以开发跨平台的移动应用,但是众所周知,它运行效率或者灵活性暂时是无法与原生应用比拟的。

    Taro

    Taro 和React一样也采用分层架构风格,只不过他们解决的问题是相反的。React加上一个分层,可以渲染到不同的视图形态;而Taro则是为了统一多样的视图形态: 国内现如今市面上端的形态多种多样,Web、React-Native、微信小程序…… 针对不同的端去编写多套代码的成本非常高,这种需求催生了Taro这类框架的诞生. 使用 Taro,我们可以只书写一套代码, 通过编译工具可以输出到不同的端:


    (图片来源: 多端统一开发框架 - Taro)

    管道和过滤器

    在管道/过滤器架构风格中,每个组件都有一组输入和输出,每个组件职责都很单一, 数据输入组件,经过内部处理,然后将处理过的数据输出。所以这些组件也称为过滤器,连接器按照业务需求将组件连接起来,其形状就像‘管道’一样,这种架构风格由此得名。


    这里面最经典的案例是*unix Shell命令,Unix的哲学就是“只做一件事,把它做好”,所以我们常用的Unix命令功能都非常单一,但是Unix Shell还有一件法宝就是管道,通过管道我们可以将命令通过标准输入输出串联起来实现复杂的功能:

     

    # 获取网页,并进行拼写检查。代码来源于wiki
    curl "http://en.wikipedia.org/wiki/Pipeline_(Unix)" | \
    sed 's/[^a-zA-Z ]/ /g' | \
    tr 'A-Z ' 'a-z\n' | \
    grep '[a-z]' | \
    sort -u | \
    comm -23 - /usr/share/dict/words | \
    less
    

    另一个和Unix管道相似的例子是ReactiveX, 例如RxJS. 很多教程将Rx比喻成河流,这个河流的开头就是一个事件源,这个事件源按照一定的频率发布事件。Rx真正强大的其实是它的操作符,有了这些操作符,你可以对这条河流做一切可以做的事情,例如分流、节流、建大坝、转换、统计、合并、产生河流的河流……

    这些操作符和Unix的命令一样,职责都很单一,只干好一件事情。但我们管道将它们组合起来的时候,就迸发了无限的能力.

    import { fromEvent } from'rxjs';
    import { throttleTime, map, scan } from'rxjs/operators';
    
    fromEvent(document, 'click')
      .pipe(
        throttleTime(1000),
        map(event => event.clientX),
        scan((count, clientX) => count + clientX, 0)
      )
      .subscribe(count =>console.log(count));
    

    除了上述的RxJS,管道模式在前端领域也有很多应用,主要集中在前端工程化领域。例如'老牌'的项目构建工具Gulp, Gulp使用管道化模式来处理各种文件类型,管道中的每一个步骤称为Transpiler(转译器), 它们以 NodeJS 的Stream 作为输入输出。整个过程高效而简单。

    不确定是否受到Gulp的影响,现代的Webpack打包工具,也使用同样的模式来实现对文件的处理, 即Loader, Loader 用于对模块的源代码进行转换, 通过Loader的组合,可以实现复杂的文件转译需求.

    // webpack.config.jsmodule.exports = {
      ...
      module: {
        rules: [{
          test: /\.scss$/,
          use: [{
              loader: "style-loader"// 将 JS 字符串生成为 style 节点
          }, {
              loader: "css-loader"// 将 CSS 转化成 CommonJS 模块
          }, {
              loader: "sass-loader"// 将 Sass 编译成 CSS
          }]
        }]
      }
    };
    

    中间件(Middleware)


    如果开发过Express、Koa或者Redux, 你可能会发现中间件模式和上述的管道模式有一定的相似性,如上图。相比管道,中间件模式可以使用一个洋葱剖面来形容。但和管道相比,一般的中间件实现有以下特点:

    • 中间件没有显式的输入输出。这些中间件之间通常通过集中式的上下文对象来共享状态

    • 有一个循环的过程。管道中,数据处理完毕后交给下游了,后面就不管了。而中间件还有一个回归的过程,当下游处理完毕后会进行回溯,所以有机会干预下游的处理结果。

    我在谷歌上搜了老半天中间件,对于中间件都没有得到一个令我满意的定义. 暂且把它当作一个特殊形式的管道模式吧。这种模式通常用于后端,它可以干净地分离出请求的不同阶段,也就是分离关注点。比如我们可以创建这些中间件:

    • 日志:记录开始时间 ⏸ 计算响应时间,输出请求日志

    • 认证:验证用户是否登录

    • 授权:验证用户是否有执行该操作的权限

    • 缓存:是否有缓存结果,有的话就直接返回 ⏸ 当下游响应完成后,再判断一下响应是否可以被缓存

    • 执行:执行实际的请求处理 ⏸ 响应

    有了中间件之后,我们不需要在每个响应处理方法中都包含这些逻辑,关注好自己该做的事情。下面是Koa的示例代码:

    const Koa = require('koa');
    const app = new Koa();
    
    // logger
    
    app.use(async (ctx, next) => {
      await next();
      const rt = ctx.response.get('X-Response-Time');
      console.log(`${ctx.method}${ctx.url} - ${rt}`);
    });
    
    // x-response-time
    
    app.use(async (ctx, next) => {
      const start = Date.now();
      await next();
      const ms = Date.now() - start;
      ctx.set('X-Response-Time', `${ms}ms`);
    });
    
    // response
    
    app.use(async ctx => {
      ctx.body = 'Hello World';
    });
    
    app.listen(3000);
    

    事件驱动

    事件驱动, 或者称为发布-订阅风格, 对于前端开发来说是再熟悉不过的概念了. 它定义了一种一对多的依赖关系, 在事件驱动系统风格中,组件不直接调用另一个组件,而是触发或广播一个或多个事件。系统中的其他组件在一个或多个事件中注册。当一个事件被触发,系统会自动通知在这个事件中注册的所有组件.

    这样就分离了关注点,订阅者依赖于事件而不是依赖于发布者,发布者也不需要关心订阅者,两者解除了耦合

    生活中也有很多发布-订阅的例子,比如微信公众号信息订阅,当新增一个订阅者的时候,发布者并不需要作出任何调整,同样发布者调整的时候也不会影响到订阅者,只要协议没有变化。我们可以发现,发布者和订阅者之间其实是一种弱化的动态的关联关系

    解除耦合目的是一方面, 另一方面也可能由基因决定的,一些事情天然就不适合或不支持用同步的方式去调用,或者这些行为是异步触发的

    JavaScript的基因决定事件驱动模式在前端领域的广泛使用. 在浏览器和Node中的JavaScript是如何工作的? 可视化解释 简单介绍了Javascript的执行原理,其中提到JavaScript是单线程的编程语言,为了应对各种实际的应用场景,一个线程以压根忙不过来的,事件驱动的异步方式是JavaScript的救命稻草.

    浏览器方面,浏览器就是一个GUI程序,GUI程序是一个循环(更专业的名字是事件循环),接收用户输入,程序处理然后反馈到页面,再接收用户输入… 用户的输入是异步,将用户输入抽象为事件是最简洁、自然、灵活的方式。

    需要注意的是:事件驱动和异步是不能划等号的。异步 !== 事件驱动,事件驱动 !== 异步

    扩展:

    • 响应式编程: 响应式编程本质上也是事件驱动的,下面是前端领域比较流行的两种响应式模式:

    • 函数响应式(Functional Reactive Programming), 典型代表RxJS

    • 透明的函数响应式编程(Transparently applying Functional Reactive Programming - TFRP), 典型代表Vue、Mobx

    • 消息总线:指接收、发送消息的软件系统。消息基于一组已知的格式,以便系统无需知道实际接收者就能互相通信

    MV*

    MV*架构风格应用也非常广泛。我觉MV*本质上也是一种分层架构,一样强调职责分离。其中最为经典的是MVC架构风格,除此之外还有各种衍生风格,例如MVPMVVMMVI(Model View Intent). 还有有点关联Flux或者Redux模式。

    家喻户晓的MVC


    如其名,MVC将应用分为三层,分别是:

    • 视图层(View) 呈现数据给用户

    • 控制器(Controller) 模型和视图之间的纽带,起到不同层的组织作用:

    • 处理事件并作出响应。一般事件有用户的行为(比如用户点击、客户端请求),模型层的变更

    • 控制程序的流程。根据请求选择适当的模型进行处理,然后选择适当的视图进行渲染,最后呈现给用户

    • 模型(Model) 封装与应用程序的业务逻辑相关的数据以及对数据的处理方法, 通常它需要和数据持久化层进行通信

    目前前端应用很少有纯粹使用MVC的,要么视图层混合了控制器层,要么就是模型和控制器混合,或者干脆就没有所谓的控制器. 但一点可以确定的是,很多应用都不约而同分离了'逻辑层'和'视图层'。

    下面是典型的AngularJS代码, 视图层:

    <h2>Todo</h2>
    <div ng-controller="TodoListController as todoList">
      <span>{{todoList.remaining()}} of {{todoList.todos.length}} remaining</span>
      [ <a href="" ng-click="todoList.archive()">archive</a> ]
      <ul class="unstyled">
        <li ng-repeat="todo in todoList.todos">
          <label class="checkbox">
            <input type="checkbox" ng-model="todo.done">
            <span class="done-{{todo.done}}">{{todo.text}}</span>
          </label>
        </li>
      </ul>
      <form ng-submit="todoList.addTodo()">
        <input type="text" ng-model="todoList.todoText"  size="30"
               placeholder="add new todo here">
        <input class="btn-primary" type="submit" value="add">
      </form>
    </div>
    

    逻辑层:

    angular.module('todoApp', [])
      .controller('TodoListController', function() {
      var todoList = this;
      todoList.todos = [
        {text:'learn AngularJS', done:true},
        {text:'build an AngularJS app', done:false}];
    
      todoList.addTodo = function() {
        todoList.todos.push({text:todoList.todoText, done:false});
        todoList.todoText = '';
      };
    
      todoList.remaining = function() {
        var count = 0;
        angular.forEach(todoList.todos, function(todo) {
          count += todo.done ? 0 : 1;
        });
        return count;
      };
    
      todoList.archive = function() {
        var oldTodos = todoList.todos;
        todoList.todos = [];
        angular.forEach(oldTodos, function(todo) {
          if (!todo.done) todoList.todos.push(todo);
        });
      };
    });
    

    至于MVP、MVVM,这些MVC模式的延展或者升级,网上都大量的资源,这里就不予赘述。

    Redux

    Redux是Flux架构的改进、融合了Elm语言中函数式的思想. 下面是Redux的架构图:


    从上图可以看出Redux架构有以下要点:

    • 单一的数据源.

    • 单向的数据流.

    单一数据源, 首先解决的是传统MVC架构多模型数据流混乱问题(如下图)。单一的数据源可以让应用的状态可预测和可被调试。另外单一数据源也方便做数据镜像,实现撤销/重做,数据持久化等等功能


    单向数据流用于辅助单一数据源, 主要目的是阻止应用代码直接修改数据源,这样一方面简化数据流,同样也让应用状态变化变得可预测。

    上面两个特点是Redux架构风格的核心,至于Redux还强调不可变数据、利用中间件封装副作用、范式化状态树,只是一种最佳实践。还有许多类Redux的框架,例如Vuex、ngrx,在架构思想层次是一致的:

    复制风格

    基于复制(Replication)风格的系统,会利用多个实例提供相同的服务,来改善服务的可访问性和可伸缩性,以及性能。这种架构风格可以改善用户可察觉的性能,简单服务响应的延迟。

    这种风格在后端用得比较多,举前端比较熟悉的例子,NodeJS. NodeJS是单线程的,为了利用多核资源,NodeJS标准库提供了一个cluster模块,它可以根据CPU数创建多个Worker进程,这些Worker进程可以共享一个服务器端口,对外提供同质的服务, Master进程会根据一定的策略将资源分配给Worker:

    const cluster = require('cluster');
    const http = require('http');
    const numCPUs = require('os').cpus().length;
    
    if (cluster.isMaster) {
      console.log(`Master ${process.pid} is running`);
    
      // Fork workers.for (let i = 0; i < numCPUs; i++) {
        cluster.fork();
      }
    
      cluster.on('exit', (worker, code, signal) => {
        console.log(`worker ${worker.process.pid} died`);
      });
    } else {
      // Workers可以共享任意的TCP连接 // 比如共享HTTP服务器 
      http.createServer((req, res) => {
        res.writeHead(200);
        res.end('hello world\n');
      }).listen(8000);
    
      console.log(`Worker ${process.pid} started`);
    }
    

    利用多核能力可以提升应用的性能和可靠性。我们也可以利用PM2这样的进程管理工具,来简化Node集群的管理,它支持很多有用的特性,例如集群节点重启、日志归集、性能监视等。

    复制风格常用于网络服务器。浏览器和Node都有Worker的概念,但是一般都只推荐在CPU密集型的场景使用它们,因为浏览器或者NodeJS内置的异步操作已经非常高效。实际上前端应用CPU密集型场景并不多,或者目前阶段不是特别实用。除此之外你还要权衡进程间通信的效率、Worker管理复杂度、异常处理等事情。

    有一个典型的CPU密集型的场景,即源文件转译. 典型的例子是CodeSandbox, 它就是利用浏览器的Worker机制来提高源文件的转译性能的:


    除了处理CPU密集型任务,对于浏览器来说,Worker也是一个重要的安全机制,用于隔离不安全代码的执行,或者限制访问浏览器DOM相关的东西。小程序抽离逻辑进程的原因之一就是安全性

    其他示例:

    • ServerLess

    微内核架构


    微内核架构(MicroKernel)又称为"插件架构", 指的是软件的内核相对较小,主要功能和业务逻辑都通过插件形式实现。内核只包含系统运行的最小功能。插件之间相互独立,插件之间的通信,应该降到最低,减少相互依赖。

     

    微内核结构的难点在于建立一套粒度合适的插件协议、以及对插件之间进行适当的隔离和解耦。从而才能保证良好的扩展性、灵活性和可迁移性。

    前端领域比较典型的例子是WebpackBabelPostCSS以及ESLint, 这些应用需要应对复杂的定制需求,而且这些需求时刻在变,只有微内核架构才能保证灵活和可扩展性。

    以Webpack为例。Webpack的核心是一个Compiler,这个Compiler主要功能是集成插件系统、维护模块对象图, 对于模块代码具体编译工作、模块的打包、优化、分析、聚合统统都是基于外部插件完成的.

    如上文说的Loader运用了管道模式,负责对源文件进行转译;那Plugin则可以将行为注入到Compiler运行的整个生命周期的钩子中, 完全访问Compiler的当前状态。

    Sean Larkin有个演讲: Everything is a plugin! Mastering webpack from the inside out

    这里还有一篇文章<微内核架构应用研究>专门写了前端微内核架构模式的一些应用,推荐阅读一下。

    微前端

    前几天听了代码时间上左耳朵耗子的一期节目, 他介绍得了亚马逊内部有很多小团队,亚马逊网站上一块豆腐块大小的区域可能是一个团队在维护,比如地址选择器、购物车、运达时间计算… 大厂的这种超级项目是怎么协调和维护的呢?这也许就是微前端或者微服务出现的原因吧。

    微前端旨在将单体前端分解成更小、更简单的模块,这些模块可以被独立的团队进行开发、测试和部署,最后再组合成一个大型的整体。


    微前端下各个应用模块是独立运行、独立开发、独立部署的,相对应的会配备更加自治的团队(一个团队干好一件事情)。微前端的实施还需要有稳固的前端基础设施和研发体系的支撑。

     

    如果你想深入学习微前端架构,建议阅读Phodal的相关文章,还有他的新书《前端架构:从入门到微前端》

    组件化架构

    组件化开发对现在的我们来说如此自然,就像水对鱼一样。以致于我们忘了组件化也是一种非常重要的架构思想,它的中心思想就是分而治之。按照Wiki上面的定义是:组件化就是基于可复用目的,将一个大的软件系统按照分离关注点的形式,拆分成多个独立的组件,主要目的就是减少耦合.

    从前端的角度具体来讲,如下图,石器时代开发方式(右侧), 组件时代(左侧):


    (图片来源: www.alloyteam.com/2015/11/we-…)
    按照Vue官网的说法: 组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树


    按照我的理解组件跟函数是一样的东西,这就是为什么函数式编程思想在React中会应用的如此自然。若干个简单函数,可以复合成复杂的函数,复杂的函数再复合成复杂的应用。对于前端来说,页面也是这么来的,一个复杂的页面就是有不同粒度的组件复合而成的。

     

    组件另外一个重要的特征就是内聚性,它是一个独立的单元,自包含了所有需要的资源。例如一个前端组件较包含样式、视图结构、组件逻辑:

    其他

    我终于编不下去了!还有很多架构风格,限于文章篇幅, 且这些风格主要应用于后端领域,这里就不一一阐述了。你可以通过扩展阅读了解这些模式

    • 面向对象风格: 将应用或系统任务分割为单独、可复用、可自给的对象,每个对象都包含数据、以及对象相关的行为

    • C/S 客户端/服务器风格

    • 面向服务架构(SOA): 指那些利用契约和消息将功能暴露为服务、消费功能服务的应用

    • N层/三层: 和分层架构差不多,侧重物理层. 例如C/S风格就是一个典型的N层架构

    • 点对点风格

    通过上文,你估计会觉得架构风格比设计模式或者算法好理解多的,正所谓‘大道至简’,但是‘简洁而不简单’!大部分项目的架构不是一开始就是这样的,它们可能经过长期的迭代,踩着巨人的肩膀,一路走过来才成为今天的样子。

    希望本文可以给你一点启发,对于我们前端工程师来说,不应该只追求能做多酷的页面、掌握多少API,要学会通过现象看本质,举一反三融会贯通,这才是进阶之道。

    文章有错误之处,请评论指出

    本文完!

    扩展阅读

    • 几种常见的软件架构风格介绍

    • 架构风格与基于网络的软件架构设计 REST提议者,Roy Thomas Fielding的博士论文

    • 软件架构入门

    • 管道 (Unix)

    • redux middleware 详解

    • 浅析前端开发中的 MVC/MVP/MVVM 模式

    • CodeSandbox 浏览器端的webpack是如何工作的?上篇

    • 万金油CS与分层

    • 大前端时代下的微前端架构:实现增量升级、代码解耦、独立部署

    • 系统组件化架构设计

    • 2015前端组件化框架之路

    展开全文
  • 软件架构风格反映了领域中众多系统所共有的结构和语义特性,并指导如何将系统中的各个模块和子系统有机的结合为一个完整的系统没多少人能记住上面的定义,需要注意的是本文不是专业讨论系统架构的文章,笔者也还没到...

    所谓软件架构风格,是指描述某个特定应用领域中系统组织方式的惯用模式。架构风格定义一个词汇表和一组约束,词汇表中包含一些组件及连接器,约束则指出系统如何将构建和连接器组合起来。软件架构风格反映了领域中众多系统所共有的结构和语义特性,并指导如何将系统中的各个模块和子系统有机的结合为一个完整的系统

    没多少人能记住上面的定义,需要注意的是本文不是专业讨论系统架构的文章,笔者也还没到那个水平. 所以暂时没必要纠结于什么是架构模式、什么是架构风格。在这里尚且把它们都当成一个系统架构上的套路, 所谓的套路就是一些通用的、可复用的,用于应对某类问题的方式方法. 可以理解为类似“设计模式”的东西,只是解决问题的层次不一样

    透过现象看本质,本文将带你领略前端领域一些流行技术栈背后的架构思想。直接进入正题吧

    文章大纲

    • 分层风格
    • Virtual DOM
    • Taro
    • 管道和过滤器
    • 中间件(Middleware)
    • 事件驱动
    • MV*
    • 家喻户晓的MVC
    • Redux
    • 复制风格
    • 微内核架构
    • 微前端
    • 组件化架构
    • 其他
    • 扩展阅读

    分层风格

    没有什么问题是分层解决不了,如果解决不了, 就再加一层 —— 鲁迅 不不,原话是: Any problem in computer science can be solved by anther layer of indirection.

    分层架构是最常见的软件架构,你要不知道用什么架构,或者不知道怎么解决问题,那就尝试加多一层。

    一个分层系统是按照层次来组织的,每一层为在其之上的层提供服务,并且使用在其之下的层所提供的服务. 分层通常可以解决什么问题

    • 是隔离业务复杂度与技术复杂度的利器. 典型的例子是网络协议, 越高层越面向人类,越底层越面向机器。一层一层往上,很多技术的细节都被隐藏了,比如我们使用HTTP时,不需要考虑TCP层的握手和包传输细节,TCP层不需要关心IP层的寻址和路由。
    6e1a4be8150f7cccbf9401749b18ece8.png
    • 分离关注点和复用。减少跨越多层的耦合, 当一层变动时不会影响到其他层。例如我们前端项目建议拆分逻辑层和视图层,一方面可以降低逻辑和视图之间的耦合,当视图层元素变动时可以尽量减少对逻辑层的影响;另外一个好处是, 当逻辑抽取出去后,可以被不同平台的视图复用。

    关注点分离之后,软件的结构会变得容易理解和开发, 每一层可以被复用, 容易被测试, 其他层的接口通过模拟解决. 但是分层架构,也不是全是优点,分层的抽象可能会丢失部分效率和灵活性, 比如编程语言就有'层次'(此例可能不太严谨),语言抽象的层次越高,一般运行效率可能会有所衰减:

    024da1806cc306a81c7d270c3654d75a.png

    分层架构在软件领域的案例实在太多太多了,咱讲讲前端的一些'分层'案例:

    我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。

    Virtual DOM

    前端石器时代,我们页面交互和渲染,是通过服务端渲染或者直接操作DOM实现的, 有点像C/C++这类系统编程语言手动操纵内存. 那时候JQuery很火:

    5a377df9ef5e2b257e5ac045dfdd6208.png

    后来随着软硬件性能越来越好、Web应用也越来越复杂,前端开发者的生产力也要跟上,类似JQuery这种命令式的编程方式无疑是比较低效的. 尽管手动操作 DOM 可能可以达到更高的性能和灵活性,但是这样对大部分开发者来说太低效了,我们是可以接受牺牲一点性能换取更高的开发效率的.

    怎么解决,再加一层吧,后来React就搞了一层VirtualDOM。我们可以声明式、组合式地构建一颗对象树, 然后交由React将它映射到DOM:

    3094482d58a8a7332324bb0a4bc43eab.png

    一开始VirtualDOM和DOM的关系比较暧昧,两者是耦合在一起的。后面有人想,我们有了VirtualDOM这个抽象层,那应该能多搞点别的,比如渲染到移动端原生组件、PDF、Canvas、终端UI等等。

    后来VirtualDOM进行了更彻底的分层,有着这个抽象层我们可以将VirtualDOM映射到更多类似应用场景:

    c530f6d2cadd0d3663fdd1cdc19d1a31.png

    所以说 VirtualDOM 更大的意义在于开发方式的转变: 声明式、 数据驱动, 让开发者不需要关心 DOM 的操作细节(属性操作、事件绑定、DOM 节点变更),换句话说应用的开发方式变成了view=f(state), 这对生产力的解放是有很大推动作用的; 另外有了VirtualDOM这一层抽象层,使得多平台渲染成为可能。

    当然VirtualDOM或者React,不是唯一,也不是第一个这样的解决方案。其他前端框架,例如Vue、Angular基本都是这样一个发展历程。

    上面说了,分层不是银弹。我们通过ReactNative可以开发跨平台的移动应用,但是众所周知,它运行效率或者灵活性暂时是无法与原生应用比拟的。

    Taro

    Taro 和React一样也采用分层架构风格,只不过他们解决的问题是相反的。React加上一个分层,可以渲染到不同的视图形态;而Taro则是为了统一多样的视图形态: 国内现如今市面上端的形态多种多样,Web、React-Native、微信小程序...... 针对不同的端去编写多套代码的成本非常高,这种需求催生了Taro这类框架的诞生. 使用 Taro,我们可以只书写一套代码, 通过编译工具可以输出到不同的端:

    5e10f54340e6345993f3f54651297982.png

    (图片来源: 多端统一开发框架 - Taro)

    管道和过滤器

    在管道/过滤器架构风格中,每个组件都有一组输入和输出,每个组件职责都很单一, 数据输入组件,经过内部处理,然后将处理过的数据输出。所以这些组件也称为过滤器,连接器按照业务需求将组件连接起来,其形状就像‘管道’一样,这种架构风格由此得名。

    19a158f1ba2fbc2c6d5b03330fc9c464.png

    这里面最经典的案例是*unix Shell命令,Unix的哲学就是“只做一件事,把它做好”,所以我们常用的Unix命令功能都非常单一,但是Unix Shell还有一件法宝就是管道,通过管道我们可以将命令通过标准输入输出串联起来实现复杂的功能:

    # 获取网页,并进行拼写检查。代码来源于wikicurl "http://en.wikipedia.org/wiki/Pipeline_(Unix)" | sed 's/[^a-zA-Z ]/ /g' | r 'A-Z ' 'a-z' | grep '[a-z]' | sort -u | comm -23 - /usr/share/dict/words | less

    另一个和Unix管道相似的例子是ReactiveX, 例如RxJS. 很多教程将Rx比喻成河流,这个河流的开头就是一个事件源,这个事件源按照一定的频率发布事件。Rx真正强大的其实是它的操作符,有了这些操作符,你可以对这条河流做一切可以做的事情,例如分流、节流、建大坝、转换、统计、合并、产生河流的河流......

    这些操作符和Unix的命令一样,职责都很单一,只干好一件事情。但我们管道将它们组合起来的时候,就迸发了无限的能力.

    import { fromEvent } from 'rxjs';import { throttleTime, map, scan } from 'rxjs/operators';fromEvent(document, 'click') .pipe( throttleTime(1000), map(event => event.clientX), scan((count, clientX) => count + clientX, 0) ) .subscribe(count => console.log(count));

    除了上述的RxJS,管道模式在前端领域也有很多应用,主要集中在前端工程化领域。例如'老牌'的项目构建工具Gulp, Gulp使用管道化模式来处理各种文件类型,管道中的每一个步骤称为Transpiler(转译器), 它们以 NodeJS 的Stream 作为输入输出。整个过程高效而简单。

    bb7fbe5611e1c41c8c319a1d6aa6ce29.png

    不确定是否受到Gulp的影响,现代的Webpack打包工具,也使用同样的模式来实现对文件的处理, 即Loader, Loader 用于对模块的源代码进行转换, 通过Loader的组合,可以实现复杂的文件转译需求.

    // webpack.config.jsmodule.exports = { ... module: { rules: [{ test: /.scss$/, use: [{ loader: "style-loader" // 将 JS 字符串生成为 style 节点 }, { loader: "css-loader" // 将 CSS 转化成 CommonJS 模块 }, { loader: "sass-loader" // 将 Sass 编译成 CSS }] }] }};

    中间件(Middleware)

    d80cd5ae60f320aa273392ea073316fc.png

    如果开发过Express、Koa或者Redux, 你可能会发现中间件模式和上述的管道模式有一定的相似性,如上图。相比管道,中间件模式可以使用一个洋葱剖面来形容。但和管道相比,一般的中间件实现有以下特点:

    • 中间件没有显式的输入输出。这些中间件之间通常通过集中式的上下文对象来共享状态
    • 有一个循环的过程。管道中,数据处理完毕后交给下游了,后面就不管了。而中间件还有一个回归的过程,当下游处理完毕后会进行回溯,所以有机会干预下游的处理结果。

    我在谷歌上搜了老半天中间件,对于中间件都没有得到一个令我满意的定义. 暂且把它当作一个特殊形式的管道模式吧。这种模式通常用于后端,它可以干净地分离出请求的不同阶段,也就是分离关注点。比如我们可以创建这些中间件:

    • 日志:记录开始时间 ⏸ 计算响应时间,输出请求日志
    • 认证:验证用户是否登录
    • 授权:验证用户是否有执行该操作的权限
    • 缓存:是否有缓存结果,有的话就直接返回 ⏸ 当下游响应完成后,再判断一下响应是否可以被缓存
    • 执行:执行实际的请求处理 ⏸ 响应

    有了中间件之后,我们不需要在每个响应处理方法中都包含这些逻辑,关注好自己该做的事情。下面是Koa的示例代码:

    const Koa = require('koa');const app = new Koa();// loggerapp.use(async (ctx, next) => { await next(); const rt = ctx.response.get('X-Response-Time'); console.log(`${ctx.method} ${ctx.url} - ${rt}`);});// x-response-timeapp.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; ctx.set('X-Response-Time', `${ms}ms`);});// responseapp.use(async ctx => { ctx.body = 'Hello World';});app.listen(3000);

    事件驱动

    事件驱动, 或者称为发布-订阅风格, 对于前端开发来说是再熟悉不过的概念了. 它定义了一种一对多的依赖关系, 在事件驱动系统风格中,组件不直接调用另一个组件,而是触发或广播一个或多个事件。系统中的其他组件在一个或多个事件中注册。当一个事件被触发,系统会自动通知在这个事件中注册的所有组件.

    这样就分离了关注点,订阅者依赖于事件而不是依赖于发布者,发布者也不需要关心订阅者,两者解除了耦合

    生活中也有很多发布-订阅的例子,比如微信公众号信息订阅,当新增一个订阅者的时候,发布者并不需要作出任何调整,同样发布者调整的时候也不会影响到订阅者,只要协议没有变化。我们可以发现,发布者和订阅者之间其实是一种弱化的动态的关联关系

    解除耦合目的是一方面, 另一方面也可能由基因决定的,一些事情天然就不适合或不支持用同步的方式去调用,或者这些行为是异步触发的

    JavaScript的基因决定事件驱动模式在前端领域的广泛使用. 在浏览器和Node中的JavaScript是如何工作的? 可视化解释 简单介绍了Javascript的执行原理,其中提到JavaScript是单线程的编程语言,为了应对各种实际的应用场景,一个线程以压根忙不过来的,事件驱动的异步方式是JavaScript的救命稻草.

    浏览器方面,浏览器就是一个GUI程序,GUI程序是一个循环(更专业的名字是事件循环),接收用户输入,程序处理然后反馈到页面,再接收用户输入... 用户的输入是异步,将用户输入抽象为事件是最简洁、自然、灵活的方式。

    需要注意的是:事件驱动和异步是不能划等号的。异步 !== 事件驱动,事件驱动 !== 异步

    扩展:

    • 响应式编程: 响应式编程本质上也是事件驱动的,下面是前端领域比较流行的两种响应式模式:
    • 函数响应式(Functional Reactive Programming), 典型代表RxJS
    • 透明的函数响应式编程(Transparently applying Functional Reactive Programming - TFRP), 典型代表Vue、Mobx
    • 消息总线:指接收、发送消息的软件系统。消息基于一组已知的格式,以便系统无需知道实际接收者就能互相通信

    MV*

    MV*架构风格应用也非常广泛。我觉MV*本质上也是一种分层架构,一样强调职责分离。其中最为经典的是MVC架构风格,除此之外还有各种衍生风格,例如MVP、MVVM、MVI(Model View Intent). 还有有点关联Flux或者Redux模式。

    家喻户晓的MVC

    9591d1689172cff12d1a34baa5e289ad.png

    如其名,MVC将应用分为三层,分别是:

    • 视图层(View) 呈现数据给用户
    • 控制器(Controller) 模型和视图之间的纽带,起到不同层的组织作用:
    • 处理事件并作出响应。一般事件有用户的行为(比如用户点击、客户端请求),模型层的变更
    • 控制程序的流程。根据请求选择适当的模型进行处理,然后选择适当的视图进行渲染,最后呈现给用户
    • 模型(Model) 封装与应用程序的业务逻辑相关的数据以及对数据的处理方法, 通常它需要和数据持久化层进行通信

    目前前端应用很少有纯粹使用MVC的,要么视图层混合了控制器层,要么就是模型和控制器混合,或者干脆就没有所谓的控制器. 但一点可以确定的是,很多应用都不约而同分离了'逻辑层'和'视图层'。

    下面是典型的AngularJS代码, 视图层:

    Todo

    {{todoList.remaining()}} of {{todoList.todos.length}} remaining [ archive ] {{todo.text}}

    逻辑层:

    angular.module('todoApp', []) .controller('TodoListController', function() { var todoList = this; todoList.todos = [ {text:'learn AngularJS', done:true}, {text:'build an AngularJS app', done:false}]; todoList.addTodo = function() { todoList.todos.push({text:todoList.todoText, done:false}); todoList.todoText = ''; }; todoList.remaining = function() { var count = 0; angular.forEach(todoList.todos, function(todo) { count += todo.done ? 0 : 1; }); return count; }; todoList.archive = function() { var oldTodos = todoList.todos; todoList.todos = []; angular.forEach(oldTodos, function(todo) { if (!todo.done) todoList.todos.push(todo); }); }; });

    至于MVP、MVVM,这些MVC模式的延展或者升级,网上都大量的资源,这里就不予赘述。

    Redux

    Redux是Flux架构的改进、融合了Elm语言中函数式的思想. 下面是Redux的架构图:

    b6ddb0f7aeb0da1a45964e68d52fbb89.png

    从上图可以看出Redux架构有以下要点:

    • 单一的数据源.
    • 单向的数据流.

    单一数据源, 首先解决的是传统MVC架构多模型数据流混乱问题(如下图)。单一的数据源可以让应用的状态可预测和可被调试。另外单一数据源也方便做数据镜像,实现撤销/重做,数据持久化等等功能

    4a1a9af678b43e09d6f353a88aa06a47.png

    单向数据流用于辅助单一数据源, 主要目的是阻止应用代码直接修改数据源,这样一方面简化数据流,同样也让应用状态变化变得可预测。

    上面两个特点是Redux架构风格的核心,至于Redux还强调不可变数据、利用中间件封装副作用、范式化状态树,只是一种最佳实践。还有许多类Redux的框架,例如Vuex、ngrx,在架构思想层次是一致的:

    ed0da4a9a554d3c0054af5a3b57ab5de.png

    复制风格

    265ba0b9b0e42175f447f52d405530ba.png

    基于复制(Replication)风格的系统,会利用多个实例提供相同的服务,来改善服务的可访问性和可伸缩性,以及性能。这种架构风格可以改善用户可察觉的性能,简单服务响应的延迟。

    这种风格在后端用得比较多,举前端比较熟悉的例子,NodeJS. NodeJS是单线程的,为了利用多核资源,NodeJS标准库提供了一个cluster模块,它可以根据CPU数创建多个Worker进程,这些Worker进程可以共享一个服务器端口,对外提供同质的服务, Master进程会根据一定的策略将资源分配给Worker:

    const cluster = require('cluster');const http = require('http');const numCPUs = require('os').cpus().length;if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); // Fork workers. for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); });} else { // Workers可以共享任意的TCP连接 // 比如共享HTTP服务器 http.createServer((req, res) => { res.writeHead(200); res.end('hello world'); }).listen(8000); console.log(`Worker ${process.pid} started`);}

    利用多核能力可以提升应用的性能和可靠性。我们也可以利用PM2这样的进程管理工具,来简化Node集群的管理,它支持很多有用的特性,例如集群节点重启、日志归集、性能监视等。

    复制风格常用于网络服务器。浏览器和Node都有Worker的概念,但是一般都只推荐在CPU密集型的场景使用它们,因为浏览器或者NodeJS内置的异步操作已经非常高效。实际上前端应用CPU密集型场景并不多,或者目前阶段不是特别实用。除此之外你还要权衡进程间通信的效率、Worker管理复杂度、异常处理等事情。

    有一个典型的CPU密集型的场景,即源文件转译. 典型的例子是CodeSandbox, 它就是利用浏览器的Worker机制来提高源文件的转译性能的:

    5d3c613c2612b32ad2d351bf03d94d37.png

    除了处理CPU密集型任务,对于浏览器来说,Worker也是一个重要的安全机制,用于隔离不安全代码的执行,或者限制访问浏览器DOM相关的东西。小程序抽离逻辑进程的原因之一就是安全性

    其他示例:

    • ServerLess

    微内核架构

    c77e2b3a23536e7bbfd470932a80aada.png

    微内核架构(MicroKernel)又称为"插件架构

    展开全文
  • 软件架构风格反映了领域中众多系统所共有的结构和语义特性,并指导如何将系统中的各个模块和子系统有机的结合为一个完整的系统没多少人能记住上面的定义,需要注意的是本文不是专业讨论系统架构的文章,笔者也还没到...
    023636527780c5fecee12963d59a8e43.png

    所谓软件架构风格,是指描述某个特定应用领域中系统组织方式的惯用模式。架构风格定义一个词汇表和一组约束,词汇表中包含一些组件及连接器,约束则指出系统如何将构建和连接器组合起来。软件架构风格反映了领域中众多系统所共有的结构和语义特性,并指导如何将系统中的各个模块和子系统有机的结合为一个完整的系统

    没多少人能记住上面的定义,需要注意的是本文不是专业讨论系统架构的文章,笔者也还没到那个水平. 所以暂时没必要纠结于什么是架构模式、什么是架构风格。在这里尚且把它们都当成一个系统架构上的套路, 所谓的套路就是一些通用的、可复用的,用于应对某类问题的方式方法. 可以理解为类似“设计模式”的东西,只是解决问题的层次不一样

    透过现象看本质,本文将带你领略前端领域一些流行技术栈背后的架构思想。直接进入正题吧

    文章大纲

    • 分层风格
    • Virtual DOM
    • Taro
    • 管道和过滤器
    • 中间件(Middleware)
    • 事件驱动
    • MV*
    • 家喻户晓的MVC
    • Redux
    • 复制风格
    • 微内核架构
    • 微前端
    • 组件化架构
    • 其他
    • 扩展阅读

    分层风格

    没有什么问题是分层解决不了,如果解决不了, 就再加一层 —— 鲁迅不不,原话是: Any problem in computer science can be solved by anther layer of indirection.

    分层架构是最常见的软件架构,你要不知道用什么架构,或者不知道怎么解决问题,那就尝试加多一层。

    一个分层系统是按照层次来组织的,每一层为在其之上的层提供服务,并且使用在其之下的层所提供的服务. 分层通常可以解决什么问题

    • 是隔离业务复杂度与技术复杂度的利器. 典型的例子是网络协议, 越高层越面向人类,越底层越面向机器。一层一层往上,很多技术的细节都被隐藏了,比如我们使用HTTP时,不需要考虑TCP层的握手和包传输细节,TCP层不需要关心IP层的寻址和路由。
    9f96363c1d7986c40ff8dc81b4093dae.png
    • 分离关注点和复用。减少跨越多层的耦合, 当一层变动时不会影响到其他层。例如我们前端项目建议拆分逻辑层和视图层,一方面可以降低逻辑和视图之间的耦合,当视图层元素变动时可以尽量减少对逻辑层的影响;另外一个好处是, 当逻辑抽取出去后,可以被不同平台的视图复用。

    关注点分离之后,软件的结构会变得容易理解和开发, 每一层可以被复用, 容易被测试, 其他层的接口通过模拟解决. 但是分层架构,也不是全是优点,分层的抽象可能会丢失部分效率和灵活性, 比如编程语言就有'层次'(此例可能不太严谨),语言抽象的层次越高,一般运行效率可能会有所衰减:

    909c999a1beff4d4cb40c8055ac4f6a6.png

    分层架构在软件领域的案例实在太多太多了,咱讲讲前端的一些'分层'案例:

    Virtual DOM

    前端石器时代,我们页面交互和渲染,是通过服务端渲染或者直接操作DOM实现的, 有点像C/C++这类系统编程语言手动操纵内存. 那时候JQuery很火:

    beff4ee930b4327f584abf67d675f4fb.png

    后来随着软硬件性能越来越好、Web应用也越来越复杂,前端开发者的生产力也要跟上,类似JQuery这种命令式的编程方式无疑是比较低效的. 尽管手动操作 DOM 可能可以达到更高的性能和灵活性,但是这样对大部分开发者来说太低效了,我们是可以接受牺牲一点性能换取更高的开发效率的.

    怎么解决,再加一层吧,后来React就搞了一层VirtualDOM。我们可以声明式、组合式地构建一颗对象树, 然后交由React将它映射到DOM:

    所以说 VirtualDOM 更大的意义在于开发方式的转变: 声明式、 数据驱动, 让开发者不需要关心 DOM 的操作细节(属性操作、事件绑定、DOM 节点变更),换句话说应用的开发方式变成了view=f(state), 这对生产力的解放是有很大推动作用的; 另外有了VirtualDOM这一层抽象层,使得多平台渲染成为可能。

    当然VirtualDOM或者React,不是唯一,也不是第一个这样的解决方案。其他前端框架,例如Vue、Angular基本都是这样一个发展历程。

    上面说了,分层不是银弹。我们通过ReactNative可以开发跨平台的移动应用,但是众所周知,它运行效率或者灵活性暂时是无法与原生应用比拟的。

    Taro

    Taro 和React一样也采用分层架构风格,只不过他们解决的问题是相反的。React加上一个分层,可以渲染到不同的视图形态;而Taro则是为了统一多样的视图形态: 国内现如今市面上端的形态多种多样,Web、React-Native、微信小程序…… 针对不同的端去编写多套代码的成本非常高,这种需求催生了Taro这类框架的诞生. 使用 Taro,我们可以只书写一套代码, 通过编译工具可以输出到不同的端:

    69c771cc77bf49d66ca175afad4be5c4.png
    72ee1eef7c4310d4fe6e7b2eed5d2e46.png

    一开始VirtualDOM和DOM的关系比较暧昧,两者是耦合在一起的。后面有人想,我们有了VirtualDOM这个抽象层,那应该能多搞点别的,比如渲染到移动端原生组件、PDF、Canvas、终端UI等等。

    后来VirtualDOM进行了更彻底的分层,有着这个抽象层我们可以将VirtualDOM映射到更多类似应用场景:

    fad5448fde8b8f75d0c74ed0992a00f9.png

    所以说 VirtualDOM 更大的意义在于开发方式的转变: 声明式、 数据驱动, 让开发者不需要关心 DOM 的操作细节(属性操作、事件绑定、DOM 节点变更),换句话说应用的开发方式变成了view=f(state), 这对生产力的解放是有很大推动作用的; 另外有了VirtualDOM这一层抽象层,使得多平台渲染成为可能。

    当然VirtualDOM或者React,不是唯一,也不是第一个这样的解决方案。其他前端框架,例如Vue、Angular基本都是这样一个发展历程。

    上面说了,分层不是银弹。我们通过ReactNative可以开发跨平台的移动应用,但是众所周知,它运行效率或者灵活性暂时是无法与原生应用比拟的。

    Taro

    Taro 和React一样也采用分层架构风格,只不过他们解决的问题是相反的。React加上一个分层,可以渲染到不同的视图形态;而Taro则是为了统一多样的视图形态: 国内现如今市面上端的形态多种多样,Web、React-Native、微信小程序…… 针对不同的端去编写多套代码的成本非常高,这种需求催生了Taro这类框架的诞生. 使用 Taro,我们可以只书写一套代码, 通过编译工具可以输出到不同的端:

    5de6f2796236cf1d3adf8eea0751df4d.png

    (图片来源: 多端统一开发框架 - Taro)

    管道和过滤器

    在管道/过滤器架构风格中,每个组件都有一组输入和输出,每个组件职责都很单一, 数据输入组件,经过内部处理,然后将处理过的数据输出。所以这些组件也称为过滤器,连接器按照业务需求将组件连接起来,其形状就像‘管道’一样,这种架构风格由此得名。

    6c6ac0899ad14cdc68adbc2dc05aaed0.png

    这里面最经典的案例是*unix Shell命令,Unix的哲学就是“只做一件事,把它做好”,所以我们常用的Unix命令功能都非常单一,但是Unix Shell还有一件法宝就是管道,通过管道我们可以将命令通过标准输入输出串联起来实现复杂的功能:

    # 获取网页,并进行拼写检查。代码来源于wikicurl "http://en.wikipedia.org/wiki/Pipeline_(Unix)" | sed 's/[^a-zA-Z ]/ /g' | tr 'A-Z ' 'a-z' | grep '[a-z]' | sort -u | comm -23 - /usr/share/dict/words | less

    另一个和Unix管道相似的例子是ReactiveX, 例如RxJS. 很多教程将Rx比喻成河流,这个河流的开头就是一个事件源,这个事件源按照一定的频率发布事件。Rx真正强大的其实是它的操作符,有了这些操作符,你可以对这条河流做一切可以做的事情,例如分流、节流、建大坝、转换、统计、合并、产生河流的河流……

    这些操作符和Unix的命令一样,职责都很单一,只干好一件事情。但我们管道将它们组合起来的时候,就迸发了无限的能力.

    import { fromEvent } from'rxjs';import { throttleTime, map, scan } from'rxjs/operators';fromEvent(document, 'click') .pipe( throttleTime(1000), map(event => event.clientX), scan((count, clientX) => count + clientX, 0) ) .subscribe(count =>console.log(count));

    除了上述的RxJS,管道模式在前端领域也有很多应用,主要集中在前端工程化领域。例如'老牌'的项目构建工具Gulp, Gulp使用管道化模式来处理各种文件类型,管道中的每一个步骤称为Transpiler(转译器), 它们以 NodeJS 的Stream 作为输入输出。整个过程高效而简单。

    2e41038ec42d34d1b79ecf6680cd2389.png

    不确定是否受到Gulp的影响,现代的Webpack打包工具,也使用同样的模式来实现对文件的处理, 即Loader, Loader 用于对模块的源代码进行转换, 通过Loader的组合,可以实现复杂的文件转译需求.

    // webpack.config.jsmodule.exports = { ... module: { rules: [{ test: /.scss$/, use: [{ loader: "style-loader"// 将 JS 字符串生成为 style 节点 }, { loader: "css-loader"// 将 CSS 转化成 CommonJS 模块 }, { loader: "sass-loader"// 将 Sass 编译成 CSS }] }] }};复制代码

    中间件(Middleware)

    e7223b17b6008919977245266bbb107d.png

    如果开发过Express、Koa或者Redux, 你可能会发现中间件模式和上述的管道模式有一定的相似性,如上图。相比管道,中间件模式可以使用一个洋葱剖面来形容。但和管道相比,一般的中间件实现有以下特点:

    • 中间件没有显式的输入输出。这些中间件之间通常通过集中式的上下文对象来共享状态
    • 有一个循环的过程。管道中,数据处理完毕后交给下游了,后面就不管了。而中间件还有一个回归的过程,当下游处理完毕后会进行回溯,所以有机会干预下游的处理结果。

    我在谷歌上搜了老半天中间件,对于中间件都没有得到一个令我满意的定义. 暂且把它当作一个特殊形式的管道模式吧。这种模式通常用于后端,它可以干净地分离出请求的不同阶段,也就是分离关注点。比如我们可以创建这些中间件:

    • 日志:记录开始时间 ⏸ 计算响应时间,输出请求日志
    • 认证:验证用户是否登录
    • 授权:验证用户是否有执行该操作的权限
    • 缓存:是否有缓存结果,有的话就直接返回 ⏸ 当下游响应完成后,再判断一下响应是否可以被缓存
    • 执行:执行实际的请求处理 ⏸ 响应

    有了中间件之后,我们不需要在每个响应处理方法中都包含这些逻辑,关注好自己该做的事情。下面是Koa的示例代码:

    const Koa = require('koa');const app = new Koa();// loggerapp.use(async (ctx, next) => { await next(); const rt = ctx.response.get('X-Response-Time'); console.log(`${ctx.method}${ctx.url} - ${rt}`);});// x-response-timeapp.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; ctx.set('X-Response-Time', `${ms}ms`);});// responseapp.use(async ctx => { ctx.body = 'Hello World';});app.listen(3000);

    事件驱动

    事件驱动, 或者称为发布-订阅风格, 对于前端开发来说是再熟悉不过的概念了. 它定义了一种一对多的依赖关系, 在事件驱动系统风格中,组件不直接调用另一个组件,而是触发或广播一个或多个事件。系统中的其他组件在一个或多个事件中注册。当一个事件被触发,系统会自动通知在这个事件中注册的所有组件.

    这样就分离了关注点,订阅者依赖于事件而不是依赖于发布者,发布者也不需要关心订阅者,两者解除了耦合

    生活中也有很多发布-订阅的例子,比如微信公众号信息订阅,当新增一个订阅者的时候,发布者并不需要作出任何调整,同样发布者调整的时候也不会影响到订阅者,只要协议没有变化。我们可以发现,发布者和订阅者之间其实是一种弱化的动态的关联关系

    解除耦合目的是一方面, 另一方面也可能由基因决定的,一些事情天然就不适合或不支持用同步的方式去调用,或者这些行为是异步触发的

    JavaScript的基因决定事件驱动模式在前端领域的广泛使用. 在浏览器和Node中的JavaScript是如何工作的? 可视化解释 简单介绍了Javascript的执行原理,其中提到JavaScript是单线程的编程语言,为了应对各种实际的应用场景,一个线程以压根忙不过来的,事件驱动的异步方式是JavaScript的救命稻草.

    浏览器方面,浏览器就是一个GUI程序,GUI程序是一个循环(更专业的名字是事件循环),接收用户输入,程序处理然后反馈到页面,再接收用户输入… 用户的输入是异步,将用户输入抽象为事件是最简洁、自然、灵活的方式。

    需要注意的是:事件驱动和异步是不能划等号的。异步 !== 事件驱动,事件驱动 !== 异步

    扩展:

    • 响应式编程: 响应式编程本质上也是事件驱动的,下面是前端领域比较流行的两种响应式模式:
    • 函数响应式(Functional Reactive Programming), 典型代表RxJS
    • 透明的函数响应式编程(Transparently applying Functional Reactive Programming - TFRP), 典型代表Vue、Mobx
    • 消息总线:指接收、发送消息的软件系统。消息基于一组已知的格式,以便系统无需知道实际接收者就能互相通信

    MV*

    MV*架构风格应用也非常广泛。我觉MV*本质上也是一种分层架构,一样强调职责分离。其中最为经典的是MVC架构风格,除此之外还有各种衍生风格,例如MVP、MVVM、MVI(Model View Intent). 还有有点关联Flux或者Redux模式。

    家喻户晓的MVC

    ce41065f2b19d704f58f1db33203576c.png

    如其名,MVC将应用分为三层,分别是:

    • 视图层(View) 呈现数据给用户
    • 控制器(Controller) 模型和视图之间的纽带,起到不同层的组织作用:
    • 处理事件并作出响应。一般事件有用户的行为(比如用户点击、客户端请求),模型层的变更
    • 控制程序的流程。根据请求选择适当的模型进行处理,然后选择适当的视图进行渲染,最后呈现给用户
    • 模型(Model) 封装与应用程序的业务逻辑相关的数据以及对数据的处理方法, 通常它需要和数据持久化层进行通信

    目前前端应用很少有纯粹使用MVC的,要么视图层混合了控制器层,要么就是模型和控制器混合,或者干脆就没有所谓的控制器. 但一点可以确定的是,很多应用都不约而同分离了'逻辑层'和'视图层'。

    下面是典型的AngularJS代码, 视图层:

    Todo

    {{todoList.remaining()}} of {{todoList.todos.length}} remaining [ archive ] {{todo.text}}

    逻辑层:

    angular.module('todoApp', []) .controller('TodoListController', function() { var todoList = this; todoList.todos = [ {text:'learn AngularJS', done:true}, {text:'build an AngularJS app', done:false}]; todoList.addTodo = function() { todoList.todos.push({text:todoList.todoText, done:false}); todoList.todoText = ''; }; todoList.remaining = function() { var count = 0; angular.forEach(todoList.todos, function(todo) { count += todo.done ? 0 : 1; }); return count; }; todoList.archive = function() { var oldTodos = todoList.todos; todoList.todos = []; angular.forEach(oldTodos, function(todo) { if (!todo.done) todoList.todos.push(todo); }); };});

    至于MVP、MVVM,这些MVC模式的延展或者升级,网上都大量的资源,这里就不予赘述。

    Redux

    Redux是Flux架构的改进、融合了Elm语言中函数式的思想. 下面是Redux的架构图:

    c98337aed9699820eddcb2d3f228452b.png

    从上图可以看出Redux架构有以下要点:

    • 单一的数据源.
    • 单向的数据流.

    单一数据源, 首先解决的是传统MVC架构多模型数据流混乱问题(如下图)。单一的数据源可以让应用的状态可预测和可被调试。另外单一数据源也方便做数据镜像,实现撤销/重做,数据持久化等等功能

    9e86f20914e2bd4fdb45d27c1677eb3e.png

    单向数据流用于辅助单一数据源, 主要目的是阻止应用代码直接修改数据源,这样一方面简化数据流,同样也让应用状态变化变得可预测。

    上面两个特点是Redux架构风格的核心,至于Redux还强调不可变数据、利用中间件封装副作用、范式化状态树,只是一种最佳实践。还有许多类Redux的框架,例如Vuex、ngrx,在架构思想层次是一致的:

    0fba9ead44f5149005c25946a2aed62d.png

    复制风格

    a354126b0d8e2b547177c7cdfd96a589.png

    基于复制(Replication)风格的系统,会利用多个实例提供相同的服务,来改善服务的可访问性和可伸缩性,以及性能。这种架构风格可以改善用户可察觉的性能,简单服务响应的延迟。

    这种风格在后端用得比较多,举前端比较熟悉的例子,NodeJS. NodeJS是单线程的,为了利用多核资源,NodeJS标准库提供了一个cluster模块,它可以根据CPU数创建多个Worker进程,这些Worker进程可以共享一个服务器端口,对外提供同质的服务, Master进程会根据一定的策略将资源分配给Worker:

    const cluster = require('cluster');const http = require('http');const numCPUs = require('os').cpus().length;if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); // Fork workers.for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); });} else { // Workers可以共享任意的TCP连接 // 比如共享HTTP服务器  http.createServer((req, res) => { res.writeHead(200); res.end('hello world'); }).listen(8000); console.log(`Worker ${process.pid} started`);}复制代码

    利用多核能力可以提升应用的性能和可靠性。我们也可以利用PM2这样的进程管理工具,来简化Node集群的管理,它支持很多有用的特性,例如集群节点重启、日志归集、性能监视等。

    复制风格常用于网络服务器。浏览器和Node都有Worker的概念,但是一般都只推荐在CPU密集型的场景使用它们,因为浏览器或者NodeJS内置的异步操作已经非常高效。实际上前端应用CPU密集型场景并不多,或者目前阶段不是特别实用。除此之外你还要权衡进程间通信的效率、Worker管理复杂度、异常处理等事情。

    有一个典型的CPU密集型的场景,即源文件转译. 典型的例子是CodeSandbox, 它就是利用浏览器的Worker机制来提高源文件的转译性能的:

    6116708fc13e1e48f577a0f06e8d95e9.png

    除了处理CPU密集型任务,对于浏览器来说,Worker也是一个重要的安全机制,用于隔离不安全代码的执行,或者限制访问浏览器DOM相关的东西。小程序抽离逻辑进程的原因之一就是安全性

    其他示例:

    • ServerLess

    微内核架构

    8a7032a60a91a28e5b3daee02315e0c2.png

    微内核架构(MicroKernel)又称为"插件架构

    展开全文
  • 1、方法定义中调用方法本身的现象2、递归注意实现1) 要有出口,否则就是死递归2) 次数不能太多,否则就内存溢出3) 构造方法不能递归使用3、递归解决问题思想和图解:分解和合并【先分解后合并】1. 常见的斐波那契...
  • 涉及诸如float或者double这两种浮点型数据处理时,偶尔总会有一些怪怪的现象,不知道大家注意过没,举几个常见的栗子: 典型现象(一):条件判断超预期 System.out.println( 1f == 0.9999999f ); // 打印:false ...
  • 常见的闭包代码陷阱

    2020-06-17 11:17:39
    如果一个函数在定义的词法环境外运行并记住了定义词法环境,这样的现象就可以称作函数闭包 function f(x=0){ var count=x; function getCount(){ return count++; } return getCount } var func = f(10)...
  • 索引定义:是一个单独,存储在磁盘上数据库结构,其包含着对数据表里所有记录引用指针. 数据库索引设计原则: 为了使索引使用效率更高,在创建索引时,必须考虑在哪些字段上创建索引和创建什么类型索引...
  • 1、方法定义中调用方法本身的现象2、递归注意实现 1) 要有出口,否则就是死递归 2) 次数不能太多,否则就内存溢出 3) 构造方法不能递归使用3、递归解决问题思想和图解: 分解和合并【先分解后合并】 1. 常见...
  • 材料疲劳 是一种结构在循环载荷作用下出现失效的现象。即使材料受到应力远低于材料静态强度,也可能会发生这种类型结构损伤。疲劳是造成机械结构失效最常见的原因。组件在反复载荷作用下导致最终失效过程,...
  • 定义 假设检验是用来判断样本与样本,样本与总体差异是由抽样误差引起还是本质差别造成统计推断方法。其基本原理是先对总体特征作出某种假设,然后通过抽样研究统计推理,对此假设应该被拒绝还是接受作出...
  • sigmoid 在定义域内处处可导,且两侧导数逐渐趋近于0。 如果X值很大或者很小时候,那么函数梯度(函数斜率)会非常小,在反向传播过程中,导致了向低层传递梯度也变得非常小。此时会出现梯度消失,现象...
  • 第十一课 常见的二叉树:基于二叉搜索树伸展树1 定义2 特性3 旋转3.1 旋转条件3.2 旋转6种情况4 代码实现 1 定义 伸展树(Splay Tree)是特殊非平衡二叉查询树,对比二叉搜索树,具备一个特点: 当某个结点被...
  • 人类定义了他们想要测量的现象,设计系统收集数据,在分析之前进行清理和预处理,最后选择如何解释结果。即使使用相同数据集,两个人也可以得出截然不同结论。这是因为数据本身并不是“地面真实”——能够反应...
  • 几种常见的假设检验

    千次阅读 2015-04-29 09:31:17
    定义 假设检验是用来判断样本与样本,样本与总体差异是由抽样误差引起还是本质差别造成统计推断方法。其基本原理是先对总体特征作出某种假设,然后通过抽样研究统计推理,对此假设应该被拒绝还是接受作出...
  • 噪声尽管是工程中最常见的现象和问题,但其物理本质却是极其精深。拿最简单一个电阻来说,不要接任何外加电源,噪声始终是存在。也就是电阻通过电流均值是0,但是其方差或者涨落不是0,数学上<I>=0, ...
  • 数据分析是一个十分重要技能,现在很多人都开始关注数据分析这个行业,同时很多企业也开始重视数据分析。但是有很多人都不是专业数据...人类定义了他们想要测量的现象,设计系统收集数据,在分析之前进行清理...
  • 1.定义:运行时检测到错误。 2.现象:当异常发生时,程序不会再向下执行,而转到函数调用语句。 3.常见异常类型: – 名称异常(NameError):变量未定义。 – 类型异常(TypeError):不同类型数据进行运算。 – ...
  • FLUKE DSX2-8000CH/DSX2-5000 插入损耗的定义对布线链路的质量评估呢,仅凭肉眼看是无法判定的,需要依据公认权威的测试标准进行全面测试。而简单的仅仅通过测试打线“线序”,线缆的长度和电阻值是否超标?有没有...
  • 先说一下现象,一般情况下C++程序,都会在.h文件中声明方法,在.cpp文件中定义方法,可是一般遇到非常简单方法就会在.h文件中声明并直接定义了。我一直是这么认为,而且认为这样做是没有问题。 但是,今天现实...
  • 方法以及变量特点关键字修饰局部变量final修饰变量初始化时机多态概述和前提条件多态好处多态弊端多态中向上转型和向下转型类转换异常...特点抽象类成员特点注意事项抽象类中小问题一个类如果没有抽象方法...
  • 知乎用户Devin 化工系线性规划问题中出现多个解的现象是非常常见的。代数解释是,目标向量是少数约束向量线性组合;几何解释是目标向量垂直于那个最优超平面。出现多解问题几种可能性:问题没有能够得到很好地...
  • IE浏览器常见的9个css Bug以及解决办法 添加评论2009年12月23日 iefans ...这让Web程序员及设计师往往为了其CSS在各个IE版本下表现怪异而痛苦不已,有时候需要通过专为IE6或者IE8设计单独的定义。IE浏览器则
  • IE6解析css的常见错误

    千次阅读 2011-09-23 13:27:18
    我们在浏览网页的时候经常看见这样的现象:某个网页在IE6浏览器中打开很正常,但是在...这让Web程序员及设计师往往为了其CSS在各个IE版本下表现怪异而痛苦不已,有时候需要通过专为IE6或者IE8设计单独的定义。IE浏览器
  • 一、文本文件与二进制文件的定义  大家都知道计算机的存储在物理上是二进制的,所以文本文件与二进制文件的区别并不是物理上的,而是逻辑上的。这两者只是在编码层次上有差异。  简单来说,文本文件是基于字符...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 446
精华内容 178
关键字:

常见现象的定义