前端工程化的持续集成

前端工程化持续集成

Posted by wang chong on February 26, 2019

什么是持续集成?

持续集成(Continuous intergration),也叫CI。

在持续集成环境中,开发人员将会频繁的提交代码到主干。这些新提交在最终合并到主线之前,都需要通过编译和自动化测试流进行验证。这样做是基于之前持续集成过程中很重视自动化测试验证结果,以保障所有的提交在合并主干之后的质量问题,对可能出现的一些问题进行预警。

什么是持续交付?

持续集成(Continuous delivery),也叫CD。

持续交付就是讲我们的应用发布出去的过程。这个过程可以确保我们尽可能快的实现交付。这就意味着除了自动化测试,我们还需要有自动化的发布流,以及通过一个按键就可以随时随地实现应用的部署上线。

通过持续交付,我们可以决定每天、每周、每两周发布一次,这完全可以根据自己的业务进行设置。但是如果真正的希望体验到持续交付的优势,就需要先进行小批量发布,尽快部署到生产线,以便在出现问题时方面进行故障排除。

什么是持续部署

持续部署(Continuous delivery),也叫CD。

如果想在持续交付的基础上更深一步的话,那就是持续部署了。通过这个方式,任何修改通过了所有的工作流就会直接和客户见面。没有人为干预(没有一键部署),只有当一个修改在工作流中构建失败才能阻止它部署到产品线。也就是说不需要人工参与,只要代码push版本仓库,线上就已经部署好了(当开发人员在主分支中合并一个提交时,这个分支将被构建、测试, 如果一切顺利,则部署到生产环境中)。

持续集成的需求–需要用持续集成来解决的问题。

  1. 持续集成是通过平台串联各个开发环节,实现和沉淀工作自动化的方法。
  2. 线上代码和代码仓库不同步,影响迭代和团队协作。
  3. 静态资源发布依赖人工,浪费开发人力。
  4. 确少自动化测试,产品质量得不到保障。
  5. 文案简单修改上线,需要技术介入。

持续集成项目的工作流程

红框框的地方主要是我们前端所做的地方。 先说一下其中的角色

  1. 开发者:也就是我们,配置本地开发环境。
  2. Git:统一代码仓库,用户管理代码。
  3. Jenkins平台:用户自动化构建、编译、部署、测试、上线。
  4. QA:测试工程师,用于测试。
  5. RD:我们的测试开发机。用于测试项目所有编译后的代码。

首先,Github上的各种命令的钩子函数被Jenkins所监控,当有开发者Push代码的时候,Jenkins收到通知,从Github上拉去代码,在Jenkins平台启动shell脚本进行自动化的编译、部署、测试、监控。完成之后,一份发给QA进行测试,一份RD(开发者)进行项目测试。

持续继承的核心技术

  1. 统一代码仓库Git,通过分支管理合并主干。
  2. 自动化构建工具,编译、部署、测试、键控、本地开发上线环境。:FIS3/Webpack/jdists/packages.json/chai/supertest/mocha/selenium-webdriver。
  3. 持续集成平台:Jenkins、Travis CI
  4. 部署工具。rsync、shelljs,yargs。
  5. 运营有权限操作运营页面保存即可上线。

统一的代码仓库。

使用Git仓库进行代码管理。

Git workflow

前端工程化

自动化编译

使用webpack编译foo.es<-foo.scss<-foo.png,编译流程

-读入foo.es的文件内容,编译成js内容
-分析js内容,找到资源定位标记‘foo.scss’
-对foo.scss进行编译:
    -读入foo.scss的文件内容,编译成css内容
    -分析css内容,找到资源定位标记'url(foo.png)'
    -对foo.png进行编译
        -读入foo.png的内容
        -图片压缩
        -返回图片内容
    -根据foo.png的最终内容计算md5戳,替换url(/static/img/foo_2af0b.png)
    -替换完毕所有资源定位标记,对css内容进行压缩
    -返回css内容
-根据foo.css的最终内容计算md5戳,替换foo.scss为/static/css/foo_bas39.css
-替换完毕所有资源定位标记,对js内容进行压缩
-返回js内容
-根据最终的js内容计算md5戳,得到foo.es的资源url为/static/scripts/foo_3fc20.js

前端模块化

前端模块化框架肩负着模块管理、资源加载两项重要的功能,这两项功能与工具、性能、业务、部署等工程环节都有非常紧密的联系。因此,模块化框架的设计应该最高优先级考虑工程需要。

CommonJs API定义很多普通应用程序(主要指非浏览器的应用)使用的API,从而填补了这个空白。它的终极目标是提供一个类似Python、Ruby和java标准库。根据这个规范,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类都是私有的,对其他文件不可见。

CMD和AMD都是CommonJS的一种规范的实现定义,RequireJS和SeaJS是对应的实践。

AMD

AMD是一种依赖先行的模块化规范,就是首先定义依赖项,然后再进行编写模块化代码。

define(['deep1','deep2'],function(dep1,dep2){
        //内部只能使用指定指定的模块。
        return ;
    })

在代码执行之前,AMD会判断所有需要的依赖,首先请求过来。

例如一下代码:

//1.js
define(['2.js'],function(dep){
    //code
    return;
})
//2.js
define(['3.js'],function(dep){
    //code
    return 2;
})
//3.js
define([],function(){
    return 3;
})

由上面代码可知:主模块为1.js,1.js中引用2.js,2.js中引用3.js。那么他们的执行和加载顺序为:

  1. 加载顺序:1.js->2.js->3.js
  2. 执行顺序:3.js->2.js->1.js
CMD

CMD是一种依赖后置的模块加载方式。也就是说按需加载,当需要什么模块的时候,就加载什么模块。

define(function(require,exports,module){
    //此处如果需要加载某XX模块,可以引入
    var  xx = require('XX')
})

CMD的模块请求方式和AMD原理是一样,CMD方式为了避免执行到浏览器网络请求的问题,内部是使用正则表达式判断代码中是否存在require,如果有先把资源加载过来。

看下面这些代码

//1.js
define(function(require,exports,module){
    var module2 = require('2.js'); 
})
//2.js
define(function(require,exports,module){
    var module3 = require('3.js');
})
//3.js
define(function(require,exports,module){
    return 3;
})

首先CMD会解析代码,判断出有使用require,首先把所需要的模块全部都请求过来。然后执行1.js,当执行到require(‘2.js’)的时候,加载2.js,执行2.js中的代码,当执行到require(‘3.js’)的时候,加载3.js,3.js执行完毕返回2.js,2.js执行完毕返回1.js,1.js执行完毕,执行结束。

AMD和CMD的优缺点
  1. CMD依赖是就近声明,通过require方法进行声明。但是因为是异步模块,加载器需要提前加载这些模块,所以模块真正使用前需要提取模块里面的所有的依赖。
  2. 不能直接压缩,require局部变量如果替换无法加载资源
  3. CMD路径参数不能进行字符串运算。
  4. AMD的依赖是提前声明。这种优势的好处就是依赖无需通过静态分析,无论是加载器还是自动化工具都可以很直接的获取到依赖。
  5. AMD依赖提前声明在代码书写上不是那么友好。
  6. AMD模块内部与NodeJS的modules有一定的差异。
新时代的前端自动化构建工具–Webpack
  1. webpack执性CommonJS标准解决了依赖配置和请求流量
  2. 对于Webpack来说万物都可以是模块,所有文件都被合并到JS中,最终在浏览器。
  3. 兼容AMD与CMD
  4. JS模块化不仅仅为了提高代码复用性,更是为了让资源文件更合理地进行缓存。

定位静态资源

古老的静态资源定位

当我们线上部署的时候,比如说我们的环境是这样的。 第一次通过浏览器访问静态资源请求是这样的。 第二次及以后请求的时候是这样的。 会清楚的发现a.css的状态码从200编程了304,而且请求资源大小也减少了,这是因为浏览器对已经存在的静态资源做了缓存。内心想着那么好,浏览器提供了缓存访问速度就增快了。

但是问题来了,当我们再去维护项目的时候,修改了a.css其中的代码,由于浏览器对之前的文件进行了缓存,所以会发现我们上线的代码根本没有用。

曾经的静态资源定位。

由于上面的问题,于是我们想到上线的时候给css加个版本号,这样的话就可以区分现在的文件和之前缓存的文件。 但是问题又来了,给每个文件都加上了版本号,项目小还可以,项目要是大了,那么多css、js文件,不累么?

累是肯定的,于是我们相当现如今是前端自动化构建工具的时代,使用自动化构建工具给每个css、js项目加上md5,这样就方面多了。

MD5是根据文件的大小及文件的内容形成的字符串。

但是问题又来了,如果我们修改了html文件中的css选择器,那么css文件也要需要修改,同时修改了两个文件。 那么上线的时候怎么上线呢?

  1. 如果先上线html,css没有跟上,那么必然有一些用户的界面是乱的。
  2. 如果先上线css,原本的css(虽然请求不同,但是文件名相同)被覆盖,html想要的选择器css中没有,那么必然有一些用户的界面是乱的。
  3. 如果一起上,两个文件的大小不同,在上线走网络的过程中一定有先后顺序,必然有一些用户的界面是乱的。
现代化的静态资源定位

现代化的静态资源定位,同样也是使用md5进行打戳,但是不是改变请求,而是改变整个文件名。 这样的方式后来上线的css文件不会覆盖之前已经存在的css文件,这样当html和css文件同时改了的时候,先上线css,后上线html,就可以了。

因为先上线css,由于之前的css存在所以不会有错误,后上线html,当HTML上线的时候,所需要的css已经在那里等着了。

这种方式特别方便回滚,实现回滚不用上线。

资源定位的优点
  1. 配置超长时间的本地缓存–节省带宽,提高性能
  2. 采用内容摘要作为缓存更新依据–精确的缓存控制
  3. 静态资源CDN部署–优化网络请求
  4. 更新资源发布路径实现非覆盖式发布–平滑升级

前端组件化

前端组件化分为四大类

  1. Custom Elements
  2. HTML Imports(废弃)
  3. HTML Templates
  4. Shadow DOM

Custom Elements

浏览器提供了一种方式,一种让开发者可以自定义HTML元素,包括特定的组成、样式和行为的方式。

<body>
    <my-element></my-element>
    <script>
         class myElement extends HTMLElement{
                constructor(){
                    super();
                }
            }
            customElements.define('my-element',myElement);
    </script>
</body>

HTML imports(废弃)

虽然废弃了,但是还要说一说。

HTML Imports是一种在HTML中引用以及复用其他的HTML文档的方式。这个Import很漂亮,可以简单理解为我们常见模板中的include之类的作用。

<head>
<link rel='import' href="/components/header.html">
</head>
<body>
    <script>
        const link = document.querySelector('link[rel="import"]')
        const header = link.import;
        const pulse = deader.querySelector('div.logo');
        //获取import的HTML的document
        const d = document.currentScript.ownerDocument
    </script>
</body>

HTML Template

用过handlebars的人都知道有这么一个东西那么HTML Templates便是把这个东西官方标准化,提供了一个template标签来存放以后需要但是暂时不渲染的HTML代码。

    <body>
         <template id="template"> <p> Smile!</p> </template>
        <script>
            let num = 3;
            const fragment = document.getElementById('template').content.cloneNode(true);
            while(num --> 1){
                fragment.firstChild.before( fragment.firstChild.cloneNode(true));
                fragment.firstChild.textContent += fragment.lastChild.textContent;
            }
            document.body.appendChild(fragment);
        </script>
    </body>

Shadow DOM

这是一种沙箱DOM的方式,创建沙箱节点,把HTML节点放在这里面,外面的css对内没有任何影响,实现css的模块化。

<body>
    <div></div>
    <script>
        const div = document.querySelector('div');
        const shadowRoot = div.createShadowRoot({mode : true});
        const span = document.createElement('span');
        span.textContent = 'hello World';
        shadowRoot.appendChild(span)
    </scirpt>
</body>

shadow-root就是shadow 根节点