关于模块化:爪哇学习笔记JavaScript模块化变迁史

1. 幼年期——无模块化按性能将js代码放到不同的JS文件,在模板中通过script标签按需援用 <script src="a.js"></script><script src="b.js"></script>文件拆散是模块化的第一步 存在的问题: 净化全局作用域 => 不利于大型项目的开发及多人团队的共建2. 成长期——命名空间模式繁多全局变量JavaScript中一个风行的命名空间模式是抉择一个全局变量作为次要的援用对象。比方jQuery库就是应用这种形式。var myApplication = (function () { function(){ //... }, return{ //... }})();命名空间前缀命名空间前缀模式其思路十分清晰,就是抉择一个独特的命名空间,而后在其前面申明申明变量、办法和对象。var myApplication_propertyA = {};var myApplication_propertyB = {};function myApplication_myMethod() {}对象字面量表示法对象字面量模式能够认为是蕴含一组键值对的对象,每一对键和值由冒号分隔,键也能够是代码新的命名空间。var myApplication = { // 能够很容易的为对象字面量定义性能 getInfo:function() { // *** }, // 能够进一步撑持对象命名空间 models:{}, views:{ pages:{} }, collections:{}};嵌套命名空间嵌套命名空间模式能够说是对象字面量模式的升级版,它也是一种无效的防止抵触模式,因为即便一个命名空间存在,它也不太可能领有同样的嵌套子对象。var myApplication = myApplication || {}; // 定义嵌套子对象 myApplication.routers = myApplication.routers || {}; myApplication.routers.test = myApplication.routers.test || {};IIFE(Immediately Invoked Function Expression,立刻调用函数表达式)IIFE实际上就是立刻执行匿名函数。在JavaScript中,因为变量和函数都是在这样一个只能在外部进行拜访的上下文中被显式地定义,函数调用提供了一种实现公有变量和办法的便捷形式。IIFE是用于封装利用程序逻辑的罕用办法,以爱护它免受全局名称空间的影响,其在命名空间方面也能够施展其非凡的作用。var namespace = namespace || {};(function( o ){ o.foo = "foo"; o.bar = function(){ return "bar"; };})(namespace);console.log(namespace);// 定义一个简略的模块const iifeModule = (() => { let count = 0; return { increase: () => ++count, reset: () => { count = 0; } }})();iifeModule.increase();iifeModule.reset();// 依赖其余模块的IIFEconst iifeModule = ((dependencyModule1, dependencyModule2) => { let count = 0; return { increase: () => ++count, reset: () => { count = 0; } }})(dependencyModule1, dependencyModule2);iifeModule.increase();iifeModule.reset();// Revealing Module(揭示模块)模式const iifeModule = (() => { let count = 0; function increaseCount() { ++count; } function resetCount() { count = 0; } return { increase: increaseCount, reset: resetCount }})();iifeModule.increase();iifeModule.reset();/** * 揭示模块模式定义: * 在模块模式的根底上,在返回的公有范畴内,从新定义所有的函数和变量。并返回一个匿名的对象。他领有所有指向公有函数的指针。 * Module模式最后被定义为一种在传统软件工程中为类提供公有和共有封装的办法。JS这里最后应用IIEF封装 **/命名空间注入命名空间注入是IIFE的另一个变体,从函数包装器外部为一个特定的命名空间“注入”办法和属性,应用this作为命名空间代理。这种模式的长处是能够将性能行为利用到多个对象或命名空间。var myApplication = myApplication || {};myApplication.utils = {};(function () { var value = 5; this.getValue = function () { return value; } // 定义新的子命名空间 this.tools = {};}).apply(myApplication.utils);(function () { this.diagnose = function () { return "diagnose"; }}).apply(myApplication.utils.tools);命名空间注入是用于为多个模块或命名空间指定一个相似的性能根本集,但最好是在申明公有变量或者办法时再应用它,其余时候应用嵌套命名空间曾经足以满足需要了。主动嵌套的命名空间function extend(ns, nsStr) { var parts = nsStr.split("."), parent = ns, pl; pl = parts.length; for (var i = 0; i < pl; i++) { // 属性如果不存在,则创立它 if (typeof parent[parts[i]] === "undefined") { parent[prats[i]] = {}; } parent = parent[parts[i]]; } return parent;}// 用法var myApplication = myApplication || {};var mod = extend(myApplication, "module.module2");3. 成熟期CJS——CommonJS每个文件就是一个模块,有本人的作用域。在一个文件外面定义的变量、函数、类,都是公有的,对其余文件不可见。如果想在多个文件分享变量,必须定义为global对象的属性。CommonJS标准规定,每个模块外部,module变量代表以后模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。require办法用于加载模块。为了不便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。 ...

March 17, 2022 · 3 min · jiezi

关于模块化:模块化演变ESM基础知识与NodejsCMS关系

模块化演变CommonJS标准 一个文件就是一个模块每个模块都有独自的作用域通过module.exports导出成员通过require函数载入模块同步模式加载模块因为同步模式加载模块,不实用浏览器,于是呈现了AMD(Asynchronous Module Definition异步模块定义标准)并呈现了Require.js,实现了AMD标准,用define定义。然而应用起来绝对简单,模块JS文件申请频繁 EsModule个性1,通过给 script 增加 type = module 的属性,就能够以 ES Module 的规范执行其中的 JS 代码了 <script type="module"> console.log('this is es module') </script>2,ESM 主动采纳严格模式,疏忽 'use strict' <script type="module"> console.log(this) // 会打印undefined </script>3,每个 ES Module 都是运行在独自的公有作用域中 <script type="module"> var foo = 100 console.log(foo) </script> <script type="module"> console.log(foo) // undefined </script>4,ESM 是通过 CORS 的形式申请内部 JS 模块的5,ESM 的 script 标签会提早执行脚本 导出导出的只是一个援用,导出的是寄存值得地址 export {name,age}导入导入中的from不能省略掉.js文件名,须要填写残缺文件路径名。也能够应用残缺URL来加载文件.import不能放在相似if这种嵌套语法中,须要放在文件顶层。如果须要动静加载,应用import()函数,返回一个promise。 import('./module.js').then(function(module){ console.log(module) // 返回对象在module中})如果导出内容很多,有多个对象,还有一个default。导入的时候其余对象能够失常导入,如果想导入import成员,须要重命名 export {a,b}export default "c";//---------------------import {a,b default as c} from "module.js"//或者如下,tilte能够随便命名import title,{a,b} from "module.js"导出导入成员我的项目中常常会呈现一些专用模块,比方component,business,page等相似的公众区域,应用的时候挨个导入会很麻烦,能够新建一个index.js文件,对立导出 ...

January 29, 2022 · 1 min · jiezi

关于模块化:模块化

模块化从本文你将理解到什么是模块化模块化的进化史当下罕用的模块化标准 CommonJS,ES ModuleES Module个性ES Module应用in Browsers , Node.jsES Modules in Node.js - 与 CommonJS 交互ES Modules in Node.js - 与 CommonJS 差别模块化前端开发范式 是一种思维依据性能不同将代码划分不同模块,从而进步开发效率,升高保护老本模块化的进化史stage-1文件划分形式 <body> <h1>模块化演变(第一阶段)</h1> <h2>基于文件的划分模块的形式</h2> <p> 具体做法就是将每个性能及其相干状态数据各自独自放到不同的文件中, 约定每个文件就是一个独立的模块, 应用某个模块就是将这个模块引入到页面中,而后间接调用模块中的成员(变量 / 函数) </p> <p> 毛病非常显著: 所有模块都间接在全局工作,没有公有空间,所有成员都能够在模块内部被拜访或者批改, 而且模块一段多了过后,容易产生命名抵触, 另外无奈治理模块与模块之间的依赖关系 </p> <script src="module-a.js"></script> <script src="module-b.js"></script> <script> // 命名抵触 method1() // 模块成员能够被批改 name = 'foo' </script></body>module-a.jsvar name = 'module-a'function method1 () { console.log(name + '#method1')}function method2 () { console.log(name + '#method2')}stage-2命名空间形式 <body> <h1>模块化演变(第二阶段)</h1> <h2>每个模块只裸露一个全局对象,所有模块成员都挂载到这个对象中</h2> <p> 具体做法就是在第一阶段的根底上,通过将每个模块「包裹」为一个全局对象的模式实现, 有点相似于为模块内的成员增加了「命名空间」的感觉。 </p> <p> 通过「命名空间」减小了命名抵触的可能, 然而同样没有公有空间,所有模块成员也能够在模块内部被拜访或者批改, 而且也无奈治理模块之间的依赖关系。 </p> <script src="module-a.js"></script> <script src="module-b.js"></script> <script> moduleA.method1() moduleB.method1() // 模块成员能够被批改 moduleA.name = 'foo' </script></body>****module-a.jsvar moduleA = { name: 'module-a', method1: function () { console.log(this.name + '#method1') }, method2: function () { console.log(this.name + '#method2') }}stage-3 IIFE形式 ...

June 5, 2021 · 4 min · jiezi

关于模块化:前端模块化

一、模块化的了解1.什么是模块?将一个简单的程序根据肯定的规定(标准)封装成几个块(文件), 并进行组合在一起块的外部数据与实现是公有的, 只是向内部裸露一些接口(办法)与内部其它模块通信2.模块化的进化过程全局function模式 : 将不同的性能封装成不同的全局函数 编码: 将不同的性能封装成不同的全局函数问题: 净化全局命名空间, 容易引起命名抵触或数据不平安,而且模块成员之间看不出间接关系function m1(){ //...}function m2(){ //...}namespace模式 : 简略对象封装 作用: 缩小了全局变量,解决命名抵触问题: 数据不平安(内部能够间接批改模块外部的数据)let myModule = { data: 'www.baidu.com', foo() { console.log(`foo() ${this.data}`) }, bar() { console.log(`bar() ${this.data}`) }}myModule.data = 'other data' //能间接批改模块外部的数据myModule.foo() // foo() other data这样的写法会裸露所有模块成员,外部状态能够被内部改写。 IIFE模式:匿名函数自调用(闭包) 作用: 数据是公有的, 内部只能通过裸露的办法操作编码: 将数据和行为封装到一个函数外部, 通过给window增加属性来向外裸露接口问题: 如果以后这个模块依赖另一个模块怎么办?// index.html文件<script type="text/javascript" src="module.js"></script><script type="text/javascript"> myModule.foo() myModule.bar() console.log(myModule.data) //undefined 不能拜访模块外部数据 myModule.data = 'xxxx' //不是批改的模块外部的data myModule.foo() //没有扭转 </script>// module.js文件(function(window) { let data = 'www.baidu.com' //操作数据的函数 function foo() { //用于裸露有函数 console.log(`foo() ${data}`) } function bar() { //用于裸露有函数 console.log(`bar() ${data}`) otherFun() //外部调用 } function otherFun() { //外部公有的函数 console.log('otherFun()') } //裸露行为 window.myModule = { foo, bar } //ES6写法})(window)最初失去的后果: ...

February 14, 2021 · 5 min · jiezi

关于模块化:Webpack模块化原理图解

Webpack模块化原理图解为什么须要模块化场景1A同学开发了模块a,B同学开发了模块b,在页面下通过如下形式进行援用 <script src="a.js"></script><script src="b.js"></script>这时模块a,模板b中的代码都裸露在全局环境中,如果模块a中定义了一个办法del。同学b并不知道,在模块b中也定义了一个办法del。这时便造成了命名抵触的的问题。 场景2C同学开发了一个公共的工具库utils.js,D同学开发了一个公共的组件tab.js,tab.js依赖utils.js。同学E须要应用D同学开发的tab.js,就须要通过如下形式援用 <script src="util.js"></script><script src="tab.js"></script>同学E本人也开发了一个dailog.js同时它也依赖util.js。当初页面同时援用了dialog.js和tab.js,代码如下 <script src="util.js"></script><script src="dialog.js"></script><scrpt src="tab.js"></script>同学E不仅须要同时援用这三个js文件,还必须保障文件之间的援用程序是正确的。同时,从下面的代码咱们无奈间接看出模块与模块之间的依赖关系,如果不深刻tab.js,咱们无奈晓得tab.js到底是只依赖util.js还是dialog.js或者两者都依赖。随着我的项目逐步增大,不同模块之间的依赖关系则会变的越来越难以保护也会导致许多模块中大量的变量都裸露在全局环境中。 模块化的几种实现计划模块化的标准有很多种, 如下| 标准 | 实现计划 | | --- | --- | | CommonJS | node.js || AMD | Require.js || CMD | Sea.js| UMD | || ES6 Module | | webpack反对CommonJS,AMD,ESModule等多种模块化形式的语法 webpack的模块化原理图解在webpack中,所有皆模块。上面咱们通过webpack来打包以下代码 目录构造如下: 代码如下: // webpack.config.jsconst path = require('path');module.exports = { entry: 'a.js', output: { path: path.resolve(__dirname, "dist"), filename: "[name].js" }, resolve: { modules: [path.resolve(__dirname)] }, optimization: { minimize: false }}// a.jsvar b = require('b');module.exports = b.text + ' world';// b.jsexports.text = 'hello';在simple目录下执行webpack命令,会在simple目录下生成dist/output.js文件。 ...

February 1, 2021 · 1 min · jiezi

小程序模块化

原本需要 24 个工作日才能完成的任务,我却在 0.5 个工作日内完成了,是如何实现的呢?趁着放假有空,写篇文章给大家分享一下我们小程序模块化加速项目开发的思路吧。阅读基础:有小程序项目经验,有查阅官方文档习惯的小伙伴 随着公司小程序项目日益繁多,仅仅靠着官方提供的框架、组件、API,已经远远不能满足项目高效迭代的要求了,于是我们组内萌生了对小程序进行模块化的想法。 实际项目中我们对小程序模块化已经涉及各个模块,我总结一下,从三个方向跟大家分享我们不一样的模块化思路:Page+,basePage,适配层。 Page+Page()作为页面的入口,我们可以通过对其入参对象的封装实现:生命周期的改造、全局状态管理和新增页面功能。 官方删除了小程序分享回调 complete,一起来尝试将其恢复吧。一般我们的逻辑是这样的: // pages/index/index.jsPage({ // 数据初始化 data: { shareFlag: false, //页面是否处于分享中 shareComplete: false //分享回调事件 }, // onShow 生命周期 onShow: function () { const { shareFlag, shareComplete } = this.data if( shareFlag ){ this.data.shareFlag = false //变量不涉及页面渲染,不使用 setData shareComplete && shareComplete() } }, // 分享事件 onShareAppMessage: function () { let shareInfo = { title: '分享测试标题', path: '', complete: function () { console.log('页面分享成功啦~') } } this.data.shareFlag = true this.data.shareComplete = typeof (shareInfo.complete) == 'function' ? shareInfo.complete : false return shareInfo }})在单页面内实现分享回调这样操作是可行的,如果多页面、多项目都要实现该功能,重复拷贝代码,则显格外得繁琐。 ...

October 2, 2019 · 3 min · jiezi

如何在Flutter上实现高性能的动态模板渲染

背景最近小组在尝试使用一套阿里dinamicX的DSL,通过动态模板下发,实现Flutter端的动态化模板渲染;本来以为只是DSL到Widget的简单映射和数据绑定,但实际跑起来的效果出乎意料的差,列表卡顿严重,帧率丢失严重。这就让我们不得不深入Flutter的Framework层,去了解Widget的创建、布局以及渲染的过程。 为什么Native可行的方案在Flutter效果这么差在iOS和Android开发中,DSL到Native的方案其实并不陌生;Android中,我们就是通过编写XML文件来描述页面布局。Native的这种映射的方案,为什么在Flutter上,效果变得如此糟糕呢? 先通过一个简单的示例来看一下dinamicX DSL的定义: 可以看到DSL的设计与Android中的XML很相似,在我们的DSL中,每个节点的width和height属性,可以赋值两种特殊意义的值:match_parent和match_content。 match_parent:当前节点大小,尽量撑开到父节点大小; match_content:当前节点大小,尽量缩小到容纳子节点大小; 在Flutter中,并没有match_parent和match_content的概念。最初我们的想法很简单,在Widget的build方法中,如果属性是match_parent,就不断向上遍历,直到找到一个父节点有确定的宽高值为止;如果是match_content,遍历所有的子节点,获取子节点大小;一旦子节点存在match_content属性,会递归调用下去。 表面上看,做好每个节点的宽高计算的缓存,虽然达不到一次性线性布局,这样的开销也并不是很大。但我们忽略掉了一个很重要的问题:Widget是immutable的,只是包含了视图的配置信息,是非常轻量级的。在Flutter中,Widget会被不断的创建销毁,这会导致布局计算非常的频繁。 要解决这些问题,单单处理Widget是不够的,需要Element以及RenderObject上做更多的处理,这也就是我们为什么要考虑自定义Widget的原因。 接下来通过源码来了解Flutter中Widget的build、layout以及paint相关的逻辑。 认识三棵树我们通过一个简单的Widget——Opacity来了解一下Widget、Element、RenderObject。 Widget在Flutter中,万物皆是Widget,Widget是immutable的,只是包含了视图的配置信息的描述,是非常轻量级的,创建和销毁的开销比较小。 Opacity继承自RenderObjectWidget,其定义了两个比较关键的函数: RenderObjectElement createElement();RenderObject createRenderObject(BuildContext context);这正是我们要找的Element和RenderObject!这里只是定义了创建的逻辑,具体调用的时机我们继续往下看。 Element在SingleChildRenderObjectWidget可以看到创建了SingleChildRenderObjectElement对象。 Element是Widget的抽象,在Widget初始化的时候,调用Widget.createElement创建,Element持有Widget和RenderObject;BuildOwner通过遍历Element Tree,根据是否标记为dirty,构建RenderObject Tree;在整个视图构建过程中,起到了串联Widget和RenderObject的作用。 RenderObjectOpacity的createRenderObject函数创建了RenderOpacity对象,RenderObject真正提供给Engine层渲染所需要的数据,RenderOpacity的Paint方法中找到了真正绘制的地方: void paint(PaintingContext context, Offset offset) { if (child != null) { ... context.pushOpacity(offset, _alpha, super.paint); } } 通过RenderObject,我们可以处理layout、painting以及hit testing。这是我们在自定义Widget处理最多的事情。RenderObject只是定义了布局的接口,并未实现布局模型,RenderBox为我们提供了2D笛卡尔坐标系下的Box模型协议定义,大部分情况下,都可以继承于RenderBox,通过重载实现一个新的layout实现,paint实现,以及点击事件处理等; Flutter在Layout过程中的优化Flutter采用一次布局的方式,O(N)的线性时间来做布局和绘制。 如上图所示,在一次遍历中,父节点调用每个子节点的布局方法,将约束向下传递,子节点根据约束,计算自己的布局,并将结果传回给父节点; RelayoutBoundary优化当一个节点满足如下条件之一,该节点会被标记为RelayoutBoundary,子节点的大小变化不会影响到父节点的布局: parentUsesSize = false:父节点的布局不依赖当前节点的大小sizedByParent = true:当前节点大小由父节点决定constraints.isTight:大小为确定的值,即宽高的最大值等于最小值parent is not RenderObject:如果父节点不是RenderObject,子节点layout变化不需要通知父节点更新RelayoutBoundary的标记,子节点大小变化,不会通知父节点重新layout,重新paint,从而提高效率。 Element更新优化为什么Widget频繁创建销毁不会影响渲染性能呢? Element定义了updateChild的方法,最早在Element被创建,Framework调用mount的时候,以及RenderObject被标记为needsLayout执行RenderObject.performLayout等场景,会调用Element的updateChild方法; Element updateChild(Element child, Widget newWidget, dynamic newSlot) { ... if (child != null) { ... if (Widget.canUpdate(child.widget, newWidget)) { ... child.update(newWidget); ... } }}对于child和newWidget都不为空的情况,通过Widget.canUpdate来判断当前child Element是否可以更新而非重现创建的方式update。 ...

September 20, 2019 · 1 min · jiezi

javascript模块化详解

一、模块化的由来1、最早我们这么写代码全部方法写在一起,容易命名冲突,并且污染global全局 function foo(){}function bar(){}2、简单封装(Namespace模式)减少了全局的变量,但是仍然可以通过myFunc.foo去操作数据,不安全 var myFunc = { _private:'no safe', foo: function(){ console.log(this._private) }}myFunc._private = 5;myFunc.foo();3、匿名闭包(IIFE模式)函数时javascript中唯一的localScope, 无法操作里面的数据 var module = (function(){ var _private = 'safe now'; var foo = function(){ console.log(_private); } return { foo:foo }})();或者(function(){ var _private = 'safe now'; var foo = function(){ console.log(_private); } window.module = { foo:foo }})()module._private; // undefined module.foo();4、增强,引入依赖有时候,我们的功能需要依赖模块才能完成,此时需要蒋模块注入进来 // 这就是模块模式的基础var module = (function($){ var _private = $('body'); var foo = function(){ console.log(_private); } return { foo:foo }})(JQuery);或者(function(global){ var _private = 'safe'; var foo = function(){ console.log(_private); } global.module = { foo:foo }})(window)module.foo();

September 20, 2019 · 1 min · jiezi

commonjs-ES-module-babel转码-webpack转码

js模块发展历程-javaScript模块七日谈前端模块化开发那点历史 #588现代ES模块也需要各种转码工具才可以在浏览器里正常运行,下面是转码现代ES模块需要了解到的知识点 commonjs & ES module & babel转码 & webpack转码 CommonJS简述CommonJS 模块输出的是一个值的 拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值了如果输出的是对象,改变其属性的话,外部引用的地方是会发生变化的如果直接改变输出的引用,那外界引用的地方是不会变化的(取缓存里面的结果)CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成commonjs 一个模块就是一个文件,require 命令第一次执行加载该脚本就会执行整个脚本,然后在内存中生成一个对象{ id: '...', // 模块名 exports: {}, // 真实的模块 loaded: true // 是否加载完毕}以后再次 require 该模块时,就会去缓存里取该对象的 exports 的属性无论 require 多少次,模块都只会运行一次,后续加载都是从缓存里面取module.exports 与 exports 的关系commonjs 规范仅仅定义了 exportsmodule.exports 是 nodejs 对 commonjs 规范的实现我们把这种实现称为 commonjs2https://github.com/webpack/webpack/issues/1114#issuecomment-105509929exports 只是在初始化对 module.exports 的引用初始化指向同一片内存空间模块导出的是 module.exports 如果对 module.exports 重新赋值,exports 上,挂的方法/属性将会失效require 引入的是 module.exports 导出的东西为避免混乱/错误,一般导出模块只建议用 module.exports 一般第三方包都用这种方式导出 modules.exports = exports = {}循环引用问题 (某个模块出现循环加载,就只输出已经执行的部分,还未执行的部分不会输出)// 代码如下// a.jsexports.A = '我是a模块';var b = require('./b.js');console.log('在 a.js 之中, 输出的 b模块==> ', b.B);exports.A = '我是后期修改过的a模块';console.log('a.js 执行完毕');// b.jsexports.B = '我是b模块';var a = require('./a.js');console.log('在 b.js 之中,输出a模块 ==>', a.A);exports.B = '我是修改后的b模块';console.log('b.js 执行完毕');// main.jsvar a = require('./a.js');var b = require('./b.js');console.log('在 main.js 之中,输出的 a模块=%j, b模块=%j', a.A, b.B);// 输出结果如下:➜ webpack-plugin git:(master) ✗ node src/babel/index 在 b.js 之中,输出a模块 ==> 我是a模块b.js 执行完毕在 a.js 之中, 输出的 b模块==> 我是修改后的b模块a.js 执行完毕在 main.js 之中,输出的 a模块="我是后期修改过的a模块", b模块="我是修改后的b模块"// 执行过程如下:执行 a.js 遇到 require b.js,暂停 a.js 执行,去执行 b.jsb.js 执行到第二行,遇到 require a.js ,从缓存中拿出刚刚 a.js 导出的模块,在 b.js 里面使用继续执行 b.js 后面的代码待 b.js 执行完毕后,控制权交还 a.js,继续执行拿到 b.js 导出的模块,在 a.js 继续使用 ... 直到结束 循环引用注意点:由于 commonjs 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是全部代码之后的值,两者可能会有差异,所以输入变量的时候必须非常小心,使用 var a = require('a') 而不是 var a = require('a').fooES6 Module基本使用export default A // 用户不需要知道导出模块的变量名import a from 'a.js'// 可以导出多个export var a = 1 // 这种方式可以直接导出一个表达式或var a = 1export {a} // 必须用花括号包起来import {a} from 'a.js'// as 关键字重命名模块export { a as A }// 导入导出合并export { default as Comps } from '../xxx'相当于import Comps from './xx'export { Comps }// 执行 loadsh 模块,但并不输出任何值import 'lodash';// 整体加载所有模块,访问时用 circle.xxx 访问import * as circle from './circle';简述: ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入,它的接口只是一种静态定义,在代码静态解析阶段就会生成。// ES6模块import { stat, exists, readFile } from 'fs';上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。由于 ES6 模块是编译时加载,使得静态分析成为可能import命令具有提升效果,会提升到整个模块的头部,首先执行import命令是编译阶段执行的,在代码运行之前。由于 import 是静态执行,所以不能使用表达式和变量(这类只有在运行时才能得到结果的语法结构)静态加载模块的好处:1. 不再需要UMD模块2. 浏览器API可以用模块格式提供,不必再做成全局变量,不再需要全局对象如:Math (可以像Python一样用模块导入)动态 import动态import() 是非常有用的。而静态型的 import 是初始化加载依赖项的最优选择,使用静态 import 更容易从代码静态分析工具和 tree shaking 中受益import(模块路径) 返回 promise,从 then 的结果里拿到加载的模块webpack 2.x 之后,有一个魔力注释的功能,会把加载的模块重命名为你注释里的文字ES6模块的浏览器加载传统方法加载js脚本script type="application/javascript"异步加载: async defer脚本异步加载,不会阻塞dom结构的解析async:加载完立即执行,渲染引擎中断,待之脚本执行完继续渲染defer:加载完会等待页面渲染完毕及页面其他脚本执行完毕才会执行多个 async 执行没有顺序保证,多个 defer 有顺序保证 es6 模块加载script type="module"浏览器对 type="module" 的处理和 defer 标志一致es6 模块的循环加载ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。es6 模块会在使用使用时才去加载对应的模块如果是循环应用,可以将对应的输出改写成函数形式,利用函数的变量提升功能CommonJS 与 ES Module 的对比// 此处是对比CommonJS 模块时运行时加载 -- 值得拷贝ES6模块时 编译时 输出接口 -- 值得引用commonjs 模块只会加载一次,以后在 碰到 require 同样的东西就从缓存里面加载如果把原模块导出的东西改变,引入模块不会跟着改变,还是从缓存里面取原来的值ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个动态的只读引用。等到真的需要用到时,再到模块里面去取值JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6的输入有点像Unix系统的“符号连接”,原始值变了,import输入的值也会跟着变。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。commonjs: module.exports = {} exports 运行阶段才加载模块,可以使用逻辑语句 模块就是对象加载的就是该对象 加载的是整个模块即将所有的接口都加载进来 输出的是值得拷贝,原模块发生变化不会影响已经加载的 this 指向当前的模块es6 模块 export 可以输出多个 {} export default 解析阶段确定对外的接口,解析阶段输出接口,不可以使用逻辑语句 加载的模块不是对象 可以单独加载其中的几个模块 静态分析,动态引用输出的是值得引用,原模块变化会影响已加载的模块 this 指向 underfinedBabel 转换 ES6 的模块化语法Babel 对 ES6 模块转码就是转换成 CommonJS 规范模块输出语法转换Babel 对于模块输出的转换,就是把所有输出都赋值到 exports 对象的属性上,并加上 ESModule: true 的标识表示这个模块是由 ESModule 转换来的 CommonJS 输出对于解构赋值输入import {a} from './a.js'转义为var _c = require('./a.js')然后取 _c.a 对于 defaultimport a from './a'import {default as a} from './a'babel转义时的处理,引入了一个 函数function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : {'default': obj}}var _a = _interopRequireDefault(require("./a.js"));console.log(_a["default"]);// 意思就是如果不是 esmodule 就为其手动添加个 default 属性,取值时统一取 default有个疑问:babel 为什么 会把 export export.default 导出的模块转换为 exports.xxx 和 exports.default 呢?而不是 module.exports ???我没有找到解释,如果您知道,麻烦给我留言下webpack 对 es6 模块和commonjs 的处理webpack本身维护了一套模块系统,这套系统兼容所有历史进程下的前端规范写一个简单的webpack配置module.exports = { entry: "./index.js", output: { path: path.resolve(__dirname, "dist"), filename: "[name].[contenthash:8].js" }, mode: "development"};执行打包命令 webpack --config webpack.config.js --env=dev 输出 main.[hash].js// 打包后代码简化如下// 首先是一个 webpack 模块运行时代码(function(modules) { // webpackBootstrap // 缓存模块 var installedModules = {}; // 函数 __webpack_require__ 参数 模块 id,用于加载和缓存模块 function __webpack_require__(moduleId) { // Check if module is in cache if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // Execute the module function modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Flag the module as loaded module.l = true; // Return the exports of the module return module.exports; } /*** 所有加载的模块都存在于 installedModules 内,其结构为: id: { id, loaded: Boolean // 是否加载过 exports // 模块的导出 } */ // 省略... 定义各种工具函数和变量 // Load entry module and return exports // 加载 entry 模块,并返回其导出,我们写的模块才会被真正执行 return __webpack_require__(__webpack_require__.s = "./index.js");})({ "./index.js": (function(module, __webpack_exports__, __webpack_require__) { // ... }, "./src/a.js": (function(module, __webpack_exports__, __webpack_require__) { // ... }, // ...})这个自调用的函数的参数 modules,就是包含所有待加载模块的一个对象{ [id: string]: Function}异步加载: import ==> webpack.requireEnsure ==> webpackJsonphttps://www.njleonzhang.com/2018/12/30/webpack-bundle-1.html其他常见问题1. babel 与 webpack 的关系webpack:将 ES6、CommonJS、amd、cmd 等模块化通过自己内部的机制统一成 webpack 的模块化。babel:转码 es6 语法,配合一些列 babel 工具链可以将新的 es2015+ 代码转换成所有浏览器支持的 es代码,babel 也可以将 es6 模块转换成 commonjs 模块2. es module 与 commonjs 为何可以混用因为 babel 会把 es module 转换成 commonjs 规范的代码babel 转码 es module 时如果遇到 export default 这种导出模块的书写方式后,会将其转换成 exports.default ,这时如果用 require 引入时,需要对其加上 .default 如 require('./a.js').a 这样才能获取 a 模块 export default 导出的 aimport 动态加载的模块也需要 .default 才能获取真实模块导出的值,如 import('./a.js').then(res => res.dafault)3. antd、element-ui 等ui框架的按需加载组件的实现需要 babel-plugin-component ...

August 18, 2019 · 4 min · jiezi

如何带领团队攻城略地优秀的架构师这样做

阿里妹导读:架构师是一个既能掌控整体又能洞悉局部瓶颈并依据具体的业务场景给出解决方案的团队领导型人物。看似完美的“人格模型”背后,是艰辛的探索。今天,阿里巴巴技术专家九摩将多年经验,进行系统性地总结,帮助更多架构师在进阶这条路上走得更“顺畅”,姿态更“优雅”。架构师职责架构师不是一个人,他需要建立高效卓越的体系,带领团队去攻城略地,在规定的时间内完成项目。 架构师需要能够识别定义并确认需求,能够进行系统分解形成整体架构,能够正确地技术选型,能够制定技术规格说明并有效推动实施落地。 按 TOGAF 的定义,架构师的职责是了解并关注实际上关系重大但未变得过载的一些关键细节和界面,架构师的角色有:理解并解析需求,创建有用的模型,确认、细化并扩展模型,管理架构。 从业界来看对于架构师的理解可以大概区分为: 企业架构师:专注于企业总体 IT 架构的设计。IT 架构师-软件产品架构师:专注于软件产品的研发。IT 架构师-应用架构师:专注于结合企业需求,定制化 IT 解决方案;大部分需要交付的工作包括总体架构、应用架构、数据架构,甚至部署架构。IT 架构师-技术架构师:专注于基础设施,某种软硬件体系,甚至云平台,提交:产品建议、产品选型、部署架构、网络方案,甚至数据中心建设方案等。阿里内部没有在职位 title 上专门设置架构师了,架构师更多是以角色而存在,现在还留下可见的 title 有两个:首席架构师和解决方案架构师,其中解决方案架构师目前在大部分 BU 都有设置,特别是在阿里云和电商体系。 解决方案架构师 工作方式理解 了解和挖掘客户痛点,项目定义,现有环境管理;梳理明确高阶需求和非功能性需求;客户有什么资产,星环(阿里电商操作系统)/阿里云等有什么解决方案;沟通,方案建议,多次迭代,交付总体架构;架构决策。职责 1.从客户视图来看: 坚定客户高层信心:利用架构和解决方案能力,帮忙客户选择星环/阿里云平台的信心。解决客户中层问题:利用星环/阿里云平台服务+结合应用架构设计/解决方案能力,帮忙客户解决业务问题,获得业务价值。引领客户 IT 员工和阿里生态同学:技术引领、方法引领、产品引领。2.从项目视图看: 对接管理部门:汇报技术方案,进度;技术沟通。对接客户 PM,项目 PM:协助项目计划,人员管理等。负责所有技术交付物的指导。对接业务部门和需求人员:了解和挖掘痛点,帮忙梳理高级业务需求,指导需求工艺。对接开发:产品支持、技术指导、架构指导。对接测试:配合测试计划和工艺制定。配合性能测试或者非功能性测试。对接运维:产品支持,运维支持。对接配置&环境:产品支持。其他:阿里技术资源聚合。3.从阿里内部看: 销售方案支持;市场宣贯;客户需求Facade;解决方案沉淀。架构师职责明确了,那么有什么架构思维可以指导架构设计呢?请看下述的架构思维。 架构思维自顶向下构建架构 要点主要如下: 1.首先定义问题,而定义问题中最重要的是定义客户的问题。定义问题,特别是识别出关键问题,关键问题是对客户有体感,能够解决客户痛点,通过一定的数据化来衡量识别出来,关键问题要优先给出解决方案。 2.问题定义务必加入时间维度,把手段/方案和问题定义区分开来。 3.问题定义中,需要对问题进行升层思考后再进行升维思考,从而真正抓到问题的本质,理清和挖掘清楚需求;要善用第一性原理思维进行分析思考问题。 4.问题解决原则:先解决客户的问题(使命),然后才能解决自己的问题(愿景);务必记住不是强调我们怎么样,而是我们能为客户具体解决什么问题,然后才是我们变成什么,从而怎么样去更好得服务客户。 5.善用多种方法对客户问题进行分析,转换成我们产品或者平台需要提供的能力,比如仓储系统 WMS 可以提供哪些商业能力。 6.对我们的现有的流程和能力模型进行梳理,找到需要提升的地方,升层思考和升维思考真正明确提升部分。 7.定义指标,并能够对指标进行拆解,然后进行数学建模。 8.将抽象出来的能力诉求转换成技术挑战,此步对于技术人员来说相当于找到了靶子,可以进行方案的设计了,需要结合自底向上的架构推导方式。 9.创新可以是业务创新,也可以是产品创新,也可以是技术创新,也可以是运营创新,升层思考、升维思考,使用第一性原理思维、生物学(进化论--进化=变异+选择+隔离、熵增定律、分形和涌现)思维等哲科思维可以帮助我们在业务,产品,技术上发现不同的创新可能。可以说哲科思维是架构师的灵魂思维。 自底向上推导应用架构 先根据业务流程,分解出系统时序图,根据时序图开始对模块进行归纳,从而得到粒度更大的模块,模块的组合/聚合构建整个系统架构。 基本上应用逻辑架构的推导有4个子路径,他们分别是: 业务概念架构:业务概念架构来自于业务概念模型和业务流程;系统模型:来自于业务概念模型;系统流程:来自业务流程;非功能性的系统支撑:来自对性能、稳定性、成本的需要。效率、稳定性、性能是最影响逻辑架构落地成物理架构的三大主要因素,所以从逻辑架构到物理架构,一定需要先对效率、稳定性和性能做出明确的量化要求。 自底向上重度依赖于演绎和归纳。 如果是产品方案已经明确,程序员需要理解这个业务需求,并根据产品方案推导出架构,此时一般使用自底向上的方法,而领域建模就是这种自底向上的分析方法。 对于自底向上的分析方法,如果提炼一下关键词,会得到如下两个关键词: 1.演绎:演绎就是逻辑推导,越是底层的,越需要演绎: 从用例到业务模型就属于演绎;从业务模型到系统模型也属于演绎;根据目前的问题,推导出要实施某种稳定性措施,这是也是演绎。2.归纳:这里的归纳是根据事物的某个维度来进行归类,越是高层的,越需要归纳: 问题空间模块划分属于归纳;逻辑架构中有部分也属于归纳;根据一堆稳定性问题,归纳出,事前,事中,事后都需要做对应的操作,是就是根据时间维度来进行归纳。 领域驱动设计架构 大部分传统架构都是基于领域模型分析架构,典型的领域实现模型设计可以参考DDD(领域驱动设计),详细可以参考《实现领域驱动设计》这本书,另外《UML和模式应用》在领域建模实操方面比较好,前者偏理论了解,后者便于落地实践。 领域划分设计步骤: 1.对用户需求场景分析,识别出业务全维度 Use Case; 2.分析模型鲁棒图,识别出业务场景中所有的实体对象。鲁棒图 —— 是需求设计过程中使用的一种方法(鲁棒性分析),通过鲁棒分析法可以让设计人员更清晰,更全面地了解需求。它通常使用在需求分析后及需求设计前做软件架构分析之用,它主要注重于功能需求的设计分析工作。需求规格说明书为其输入信息,设计模型为其输出信息。它是从功能需求向设计方案过渡的第一步,重点是识别组成软件系统的高级职责模块、规划模块之间的关系。鲁棒图包含三种图形:边界、控制、实体,三个图形如下: ...

July 4, 2019 · 3 min · jiezi

就是要你懂负载均衡lvs和转发模式

本文希望阐述清楚LVS的各种转发模式,以及他们的工作流程和优缺点,同时从网络包的流转原理上解释清楚优缺点的来由,并结合阿里云的slb来说明优缺点。如果对网络包是怎么流转的不太清楚,推荐先看这篇基础:程序员的网络知识 -- 一个网络包的旅程,对后面理解LVS的各个转发模式非常有帮助。 几个术语和缩写cip:Client IP,客户端地址vip:Virtual IP,LVS实例IPrip:Real IP,后端RS地址RS: Real Server 后端真正提供服务的机器LB: Load Balance 负载均衡器LVS: Linux Virtual Serversip: source ipdip: destinationLVS的几种转发模式DR模型 -- (Director Routing-直接路由)NAT模型 -- (NetWork Address Translation-网络地址转换)fullNAT -- (full NAT)ENAT --(enhence NAT 或者叫三角模式/DNAT,阿里云提供)IP TUN模型 -- (IP Tunneling - IP隧道)DR模型(Director Routing--直接路由) 如上图所示基本流程(假设 cip 是200.200.200.2, vip是200.200.200.1): 请求流量(sip 200.200.200.2, dip 200.200.200.1) 先到达 LVS然后LVS,根据负载策略挑选众多 RS中的一个,然后将这个网络包的MAC地址修改成这个选中的RS的MAC然后丢给交换机,交换机将这个包丢给选中的RS选中的RS看到MAC地址是自己的、dip也是自己的,愉快地手下并处理、回复回复包(sip 200.200.200.1, dip 200.200.200.2)经过交换机直接回复给client了(不再走LVS)我们看到上面流程,请求包到达LVS后,LVS只对包的目的MAC地址作了修改,回复包直接回给了client。 同时还能看到多个RS和LVS都共用了同一个IP但是用的不同的MAC,在二层路由不需要IP,他们又在同一个vlan,所以这里没问题。 RS上会将vip配置在lo回环网卡上,同时route中添加相应的规则,这样在第四步收到的包能被os正常处理。 优点: DR模式是性能最好的一种模式,入站请求走LVS,回复报文绕过LVS直接发给Client缺点: 要求LVS和rs在同一个vlan;RS需要配置vip同时特殊处理arp;不支持端口映射。为什么要求LVS和RS在同一个vlan(或者说同一个二层网络里)因为DR模式依赖多个RS和LVS共用同一个VIP,然后依据MAC地址来在LVS和多个RS之间路由,所以LVS和RS必须在一个vlan或者说同一个二层网络里 DR 模式为什么性能最好因为回复包不走LVS了,大部分情况下都是请求包小,回复包大,LVS很容易成为流量瓶颈,同时LVS只需要修改进来的包的MAC地址。 DR 模式为什么回包不需要走LVS了因为RS和LVS共享同一个vip,回复的时候RS能正确地填好sip为vip,不再需要LVS来多修改一次(后面讲的NAT、Full NAT都需要) 总结下 DR的结构 ...

July 4, 2019 · 1 min · jiezi

使用NGINX作为HTTPS正向代理服务器

NGINX主要设计作为反向代理服务器,但随着NGINX的发展,它同样能作为正向代理的选项之一。正向代理本身并不复杂,而如何代理加密的HTTPS流量是正向代理需要解决的主要问题。本文将介绍利用NGINX来正向代理HTTPS流量两种方案,及其使用场景和主要问题。 HTTP/HTTPS正向代理的分类简单介绍下正向代理的分类作为理解下文的背景知识: 按客户端有无感知的分类普通代理:在客户端需要在浏览器中或者系统环境变量手动设置代理的地址和端口。如squid,在客户端指定squid服务器IP和端口3128。透明代理:客户端不需要做任何代理设置,“代理”这个角色对于客户端是透明的。如企业网络链路中的Web Gateway设备。按代理是否解密HTTPS的分类隧道代理 :也就是透传代理。代理服务器只是在TCP协议上透传HTTPS流量,对于其代理的流量的具体内容不解密不感知。客户端和其访问的目的服务器做直接TLS/SSL交互。本文中讨论的NGINX代理方式属于这种模式。中间人(MITM, Man-in-the-Middle)代理:代理服务器解密HTTPS流量,对客户端利用自签名证书完成TLS/SSL握手,对目的服务器端完成正常TLS交互。在客户端-代理-服务器的链路中建立两段TLS/SSL会话。如Charles,简单原理描述可以参考文章。注:这种情况客户端在TLS握手阶段实际上是拿到的代理服务器自己的自签名证书,证书链的验证默认不成功,需要在客户端信任代理自签证书的Root CA证书。所以过程中是客户端有感的。如果要做成无感的透明代理,需要向客户端推送自建的Root CA证书,在企业内部环境下是可实现的。为什么正向代理处理HTTPS流量需要特殊处理?作为反向代理时,代理服务器通常终结 (terminate) HTTPS加密流量,再转发给后端实例。HTTPS流量的加解密和认证过程发生在客户端和反向代理服务器之间。 而作为正向代理在处理客户端发过来的流量时,HTTP加密封装在了TLS/SSL中,代理服务器无法看到客户端请求URL中想要访问的域名,如下图。所以代理HTTPS流量,相比于HTTP,需要做一些特殊处理。 NGINX的解决方案根据前文中的分类方式,NGINX解决HTTPS代理的方式都属于透传(隧道)模式,即不解密不感知上层流量。具体的方式有如下7层和4层的两类解决方案。 HTTP CONNECT隧道 (7层解决方案)历史背景早在1998年,也就是TLS还没有正式诞生的SSL时代,主导SSL协议的Netscape公司就提出了关于利用web代理来tunneling SSL流量的INTERNET-DRAFT。其核心思想就是利用HTTP CONNECT请求在客户端和代理之间建立一个HTTP CONNECT Tunnel,在CONNECT请求中需要指定客户端需要访问的目的主机和端口。Draft中的原图如下: 整个过程可以参考HTTP权威指南中的图: 客户端给代理服务器发送HTTP CONNECT请求。代理服务器利用HTTP CONNECT请求中的主机和端口与目的服务器建立TCP连接。代理服务器给客户端返回HTTP 200响应。客户端和代理服务器建立起HTTP CONNECT隧道,HTTPS流量到达代理服务器后,直接通过TCP透传给远端目的服务器。代理服务器的角色是透传HTTPS流量,并不需要解密HTTPS。 NGINX ngx_http_proxy_connect_module模块NGINX作为反向代理服务器,官方一直没有支持HTTP CONNECT方法。但是基于NGINX的模块化、可扩展性好的特性,阿里的@chobits提供了ngx_http_proxy_connect_module模块,来支持HTTP CONNECT方法,从而让NGINX可以扩展为正向代理。 环境搭建以CentOS 7的环境为例。 1) 安装对于新安装的环境,参考正常的安装步骤和安装这个模块的步骤(https://github.com/chobits/ngx_http_proxy_connect_module)),把对应版本的patch打上之后,在configure的时候加上参数--add-module=/path/to/ngx_http_proxy_connect_module,示例如下: ./configure \--user=www \--group=www \--prefix=/usr/local/nginx \--with-http_ssl_module \--with-http_stub_status_module \--with-http_realip_module \--with-threads \--add-module=/root/src/ngx_http_proxy_connect_module对于已经安装编译安装完的环境,需要加入以上模块,步骤如下: # 停止NGINX服务# systemctl stop nginx# 备份原执行文件# cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak# 在源代码路径重新编译# cd /usr/local/src/nginx-1.16.0./configure \--user=www \--group=www \--prefix=/usr/local/nginx \--with-http_ssl_module \--with-http_stub_status_module \--with-http_realip_module \--with-threads \--add-module=/root/src/ngx_http_proxy_connect_module# make# 不要make install# 将新生成的可执行文件拷贝覆盖原来的nginx执行文件# cp objs/nginx /usr/local/nginx/sbin/nginx# /usr/bin/nginx -Vnginx version: nginx/1.16.0built by gcc 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)built with OpenSSL 1.0.2k-fips 26 Jan 2017TLS SNI support enabledconfigure arguments: --user=www --group=www --prefix=/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-threads --add-module=/root/src/ngx_http_proxy_connect_module2) nginx.conf文件配置 ...

June 24, 2019 · 5 min · jiezi

做可交互的统计图表这套图形语法不容错过

选好可视化“一图胜千言”,是最直观的数据可视化魅力。以图表来传达和沟通信息,其效率远超枯燥乏味的数据表达。 有需求就有市场。数据可视化崭露头角后,各个厂商出备的产品、解决方案,开发者自研的可视化工具、操作平台都如雨后春笋般冒了出来。 受众不同,个人的选择就会不同;需求不同,特色的选择就会不同。但选择繁多,很多开发者和企业就会头疼:有数据可视化的需求,但工具到底该如何选择? AntV-G2是阿里巴巴2018年推出的开源项目,是一套基于可视化编码的图形语法,具有高度的易用性和扩展性。无需关注繁琐的实现细节,一条语句即可构建出各种各样的可交互统计图表。它具备以下特性: 简单、易用:从数据出发,仅需几行代码就能轻松获得想要的图表展示效果完备的可视化编码:以数据驱动,提供从数据到图形的完整映射强大的扩展能力:任何图表,都可以基于图形语法灵活绘制,满足无限创意作为一个非常全面的图表库,AntV G2库有折线图、柱状图、条形图、雷达图、箱体图、面积图、饼图、热力图、仪表盘… …几乎满足了所有基本的图表类需求。 另外,G2还是一个使用WebGL/canvas技术实现的基础图表库,因此既可以在原生js环境下使用,也可以使用任意的js框架。基于G2封装的组件框架有BizCharts和Viser,所以如果使用angular、react、vue的话可以直接使用其封装的组件,和自行动手封装G2组件是一样的效果。 G2的构成一个可视化框架需要四部分: 数据处理模块,对数据进行加工的模块,包括一些数据处理方法。例如:合并、分组、排序、过滤、计算统计信息等图形映射模块,将数据映射到图形视觉通道的过程。例如:将数据映射成颜色、位置、大小等图形展示模块,决定使用何种图形来展示数据,点、线、面等图形标记辅助信息模块,用于说明视觉通道跟数据的映射关系,例如:坐标轴、图例、辅助文本等 在数据处理模块上,dataSet主要通过state状态管理多个dataview视图,实现多图联动,或者关联视图。dataView则是对应的是每一个数据源,通过connector来接入不同类型的数据,通过tranform进行数据的转换或者过滤。最后输出我们理想的数据,dataSet是与g2分离的,需要用到的时候可以加载;在图形映射模块上,度量 Scale,是数据空间到图形空间的转换桥梁,负责原始数据到 [0, 1] 区间数值的相互转换工作,从原始数据到 [0, 1] 区间的转换我们称之为归一化操作。我们可以通过chart.source或者chart.scale('field', defs)来实现列定义,我们可以在这对数据进行起别名,更换显示类型(time,cat类型等);辅助信息,就是标记数据,方便理解数据;图形展示chart图表是一个大画布,可以有多个view视图,geom则是数据映射的图形标识,就是指的点,线,面,通过对其操作,从而展示图形。大体步骤如下: G2 经典新生目前AntV-G2已更新到3.4版本。通过这次升级,G2往经典的“图形语法”理论注入了新的生命,为大家带来“交互语法” — 一套简洁高效的交互式可视化解决方案。同时,G2的底层渲染进行了升级,实现 SVG 和 Canvas 自由切换。 简洁灵活的交互语法G2将经典的图形语法理论扩展为“交互语法”,一方面开放 220+ 种交互事件,支持定制最小粒度的图表元素交互,另一方面封装了各类复杂的、常用的交互场景,使丰富灵活的图表交互仅需一行代码实现。 渲染引擎自由切换G2的绘图引擎开始支持 SVG 和 Canvas 双引擎,以适应更多业务场景。并在拾取、动画管线、碰撞检测等方面进行了优化,G2的绘图能力变得更自由、更流畅。 两种引擎在不同场景的性能对比 256+58的试炼通过256 plots计划和58+业务模板计划,来向用户提供更丰富的场景,也由此检验G2图表的数据表达能力。 通过256 plots计划,G2挑战了d3.js、R语言社区等经典图表绘制,检验并刺激了G2框架图形能力的更新。 58+业务模板源自真实的业务,由基础的线、柱、饼图表改造而起,进而辐射到分面、迷你图等更复杂的场景,能更好的帮助用户找到理想的可视化解决方案。 DataV数据可视化AntV-G2功能虽然强大,但对于需要开箱即用、直接适用业务的企业而言,距离可视化还缺少一个成熟的产品。幸运的是,阿里云.DataV数据可视化完美承担了这样的一个角色。DataV只需通过拖拽式的操作,使用数据连接、可视化组件库、行业设计模板库、多终端适配与发布运维于等功能,就能让非专业的人员快速地将数据价值通过视觉来传达。 DataV具有丰富的图表库,并外接有国内两大第三方图表组件库——Echarts和今日的主角:AntV-G2。在强大的图表库支持下,DataV可以制作出丰富多样的可视化页面,随心所欲自由搭配图表来做组合。 本文作者:数据智能小二原文链接 本文为云栖社区原创内容,未经允许不得转载。

June 10, 2019 · 1 min · jiezi

移动研发-DevOps-落地实践

作者:姚兰天(十镜),蚂蚁金服技术专家。概要:传统的研发模式已经无法适应企业在数字化转型中快速迭代以及研发协同的要求,建设符合业务场景特性和有效支撑高并发、持续迭代集成需求的研发效能实践迫在眉睫。本文将围绕支付宝如何随着移动市场的高速发展,逐步沉淀优化出适用业务发展需求的研发效能实践。 现场视频):http://t.cn/Ai9HuCNT 大家好,我是来自支付宝终端工程技术团队的十境。本文将带领大家了解支付宝移动端如何随着移动市场的告诉发展,逐步沉淀优化出适用业务发展需求的研发效能实践。 0. 背景如何解决百万级代码的极速构建?如何让上百开发者在同一个 App 上高效研发协同?如何保障代码频繁变更下的交付质量?显然,传统的研发模式已经无法适应企业在数字化转型中快速迭代以及研发协同的要求,建设符合业务场景特性和有效支撑高并发、持续迭代集成需求的研发效能实践迫在眉睫。 1. 研发协作平台现状关于支付宝在移动端研发平台构建的历程,首先我们先展开看看目前平台的现状,并讲述如何参考 DevOps “三步工作法” 来正向建模我们的交付价值流,以及这些活动中比较核心的分支模型,构建,持续集成等。 研发协作平台大概从 2014 年开始建设,如今支持的 iOS 和 Android 客户端代码量都已经超过 300w 行,拆分的 Bundle 数量也都在 300 个以上。我们每周的构建次数在 1.4W,安装包平均每天会灰度 2~3 次,开发测试同学达到近千人的规模。 我们支撑了蚂蚁集团支付宝、网商银行、财富、口碑等产品的交付,支持的技术栈从最开始的 Android 和 iOS,演进到厂商 SDK、小程序、IoT 及桌面应用等。在这些能力输出的下层是我们沉淀的一套研发协作流程,从需求到开发、测试、交付、及发布后的反馈闭环。 支付宝业务的飞速发展,从工具到超级 App,代码量猛增到 300W+。技术架构上,采用了模块化动态加载的技术,这就给我们提了一个问题,如何将 300+ 个 Bundle,在不同的团队里开发,集成,变成一个高质量的 App 推送到用户手机上。 2. DevOps 三步工作法 DevOps 三步工作法,第一步,我们正向价值流建模,把研发划分为 5 个阶段(需求阶段、开发阶段、测试阶段、集成阶段以及发布阶段),定义每个阶段的准入准出标准。比如需求分析的结果需要拆分到 User Story 级别,通过大家需求评审,达成一致。接着,每个阶段我们提炼出最重要的活动,比如开发阶段,开发同学每天最多的就是写代码,代码 Review,以及代码 MR/Push 后触发的自动化流水线,如编译、扫描、自动化测试等。这些阶段和每个阶段的活动以及人员之间的协作,就构成了我们交付大图的脉络,即我们常说的价值流。 通过正向价值流的建模,结合团队的开发实践,便可以得到研发协作平台产品的一个信息架构图。 如上图所示,随时间演进,我们沉淀出了一套产品信息图:从最开始仅仅是安装包构建的一个在线工具,到产物管理,版本管理,架构拆分后的模块信息、模块构建管理,根据构建的产物及场景的不同,抽象出了构建配置、渠道配置、持续集成的配置,当然还有其它元数据如证书信息的配置。 我们参考了敏捷、Scrum 实践,抽象出迭代的概念来组织每个模块涉及的资源如代码仓库、需求、缺陷、任务、持续集成流水线还有最重要的团队和人员。发布定义了我们交付的产物,同时也是各团队工作集成到一起的大容器。 这是我们研发协作平台的门户首页,开发者能直观地看到自己关注项目的日常发布、迭代信息,以及每天需要解决的待办等,每个类目和我们上一页提炼的信息架构相对应。 拆解「依赖配置」 前面提到我们通过架构拆分,团队模块化协作的方式来应对激增的业务需求。那么之所以有这张截图,是想让大家对我们的依赖配置有个直观的感受,每个模块的产物可以理解为一个 Zip 包,在某一个安装包发布中管理这样由 300 多个 Bundle 构成的一个依赖列表。我们的需求集成某种意义上就是这个依赖列表中中模块版本的升级。模块拆分也让我们的小批量快速交付成为得以践行、拥有 2 周发布一个大版本的能力。 ...

June 10, 2019 · 1 min · jiezi

做可交互的统计图表这套图形语法不容错过

选好可视化“一图胜千言”,是最直观的数据可视化魅力。以图表来传达和沟通信息,其效率远超枯燥乏味的数据表达。 有需求就有市场。数据可视化崭露头角后,各个厂商出备的产品、解决方案,开发者自研的可视化工具、操作平台都如雨后春笋般冒了出来。 受众不同,个人的选择就会不同;需求不同,特色的选择就会不同。但选择繁多,很多开发者和企业就会头疼:有数据可视化的需求,但工具到底该如何选择? AntV-G2是阿里巴巴2018年推出的开源项目,是一套基于可视化编码的图形语法,具有高度的易用性和扩展性。无需关注繁琐的实现细节,一条语句即可构建出各种各样的可交互统计图表。它具备以下特性: 简单、易用:从数据出发,仅需几行代码就能轻松获得想要的图表展示效果完备的可视化编码:以数据驱动,提供从数据到图形的完整映射强大的扩展能力:任何图表,都可以基于图形语法灵活绘制,满足无限创意作为一个非常全面的图表库,AntV G2库有折线图、柱状图、条形图、雷达图、箱体图、面积图、饼图、热力图、仪表盘… …几乎满足了所有基本的图表类需求。 另外,G2还是一个使用WebGL/canvas技术实现的基础图表库,因此既可以在原生js环境下使用,也可以使用任意的js框架。基于G2封装的组件框架有BizCharts和Viser,所以如果使用angular、react、vue的话可以直接使用其封装的组件,和自行动手封装G2组件是一样的效果。 G2的构成一个可视化框架需要四部分: 数据处理模块,对数据进行加工的模块,包括一些数据处理方法。例如:合并、分组、排序、过滤、计算统计信息等图形映射模块,将数据映射到图形视觉通道的过程。例如:将数据映射成颜色、位置、大小等图形展示模块,决定使用何种图形来展示数据,点、线、面等图形标记辅助信息模块,用于说明视觉通道跟数据的映射关系,例如:坐标轴、图例、辅助文本等 在数据处理模块上,dataSet主要通过state状态管理多个dataview视图,实现多图联动,或者关联视图。dataView则是对应的是每一个数据源,通过connector来接入不同类型的数据,通过tranform进行数据的转换或者过滤。最后输出我们理想的数据,dataSet是与g2分离的,需要用到的时候可以加载;*  在图形映射模块上,度量 Scale,是数据空间到图形空间的转换桥梁,负责原始数据到 [0, 1] 区间数值的相互转换工作,从原始数据到 [0, 1] 区间的转换我们称之为归一化操作。我们可以通过chart.source或者chart.scale('field', defs)来实现列定义,我们可以在这对数据进行起别名,更换显示类型(time,cat类型等); *  辅助信息,就是标记数据,方便理解数据; *  图形展示chart图表是一个大画布,可以有多个view视图,geom则是数据映射的图形标识,就是指的点,线,面,通过对其操作,从而展示图形。 大体步骤如下: G2 经典新生目前AntV-G2已更新到3.4版本。通过这次升级,G2往经典的“图形语法”理论注入了新的生命,为大家带来“交互语法” — 一套简洁高效的交互式可视化解决方案。同时,G2的底层渲染进行了升级,实现 SVG 和 Canvas 自由切换。 简洁灵活的交互语法 G2将经典的图形语法理论扩展为“交互语法”,一方面开放 220+ 种交互事件,支持定制最小粒度的图表元素交互,另一方面封装了各类复杂的、常用的交互场景,使丰富灵活的图表交互仅需一行代码实现。 渲染引擎自由切换 G2的绘图引擎开始支持 SVG 和 Canvas 双引擎,以适应更多业务场景。并在拾取、动画管线、碰撞检测等方面进行了优化,G2的绘图能力变得更自由、更流畅。 两种引擎在不同场景的性能对比 256+58的试炼 通过256 plots计划和58+业务模板计划,来向用户提供更丰富的场景,也由此检验G2图表的数据表达能力。 通过256 plots计划,G2挑战了d3.js、R语言社区等经典图表绘制,检验并刺激了G2框架图形能力的更新。 58+业务模板源自真实的业务,由基础的线、柱、饼图表改造而起,进而辐射到分面、迷你图等更复杂的场景,能更好的帮助用户找到理想的可视化解决方案。 DataV数据可视化AntV-G2功能虽然强大,但对于需要开箱即用、直接适用业务的企业而言,距离可视化还缺少一个成熟的产品。幸运的是,阿里云.DataV数据可视化完美承担了这样的一个角色。DataV只需通过拖拽式的操作,使用数据连接、可视化组件库、行业设计模板库、多终端适配与发布运维于等功能,就能让非专业的人员快速地将数据价值通过视觉来传达。 DataV具有丰富的图表库,并外接有国内两大第三方图表组件库——Echarts和今日的主角:AntV-G2。在强大的图表库支持下,DataV可以制作出丰富多样的可视化页面,随心所欲自由搭配图表来做组合。 本文作者:数据智能小二阅读原文 本文为云栖社区原创内容,未经允许不得转载。

June 6, 2019 · 1 min · jiezi

为什么kill进程后socket一直处于FINWAIT1状态

本文介绍一个因为conntrack内核参数设置和iptables规则设置的原因导致TCP连接不能正常关闭(socket一直处于FIN_WAIT_1状态)的案例,并介绍conntrack相关代码在conntrack表项超时后对新报文的处理逻辑。 案例现象问题的现象: ECS上有一个进程,建立了到另一个服务器的socket连接。 kill掉进程,发现tcpdump抓不到FIN包发出,导致服务器端的连接没有正常关闭。为什么有这种现象呢? 梳理正常情况下kill进程后,用户态调用close()系统调用来发起TCP FIN给对端,所以这肯定是个异常现象。关键的信息是: 用户态kill进程。ECS网卡层面没有抓到FIN包。从这个现象描述中可以推断问题出在位于用户空间和网卡驱动中间的内核态中。但是是系统调用问题,还是FIN已经构造后出的问题,还不确定。这时候比较简单有效的判断的方法是看socket的状态。socket处于TIME_WAIT_1状态,这个信息很有用,可以判断系统调用是正常的,因为按照TCP状态机,FIN发出来后socket会进入TIME_WAIT_1状态,在收到对端ACK后进入TIME_WAIT_2状态。关于socket的另一个信息是:这个socket长时间处于TIME_WAIT_1状态,这也反向证明了在网卡上没有抓到FIN包的陈述是合理。FIN包没出虚机网卡,对端收不到FIN,所以自然没有机会回ACK。 真凶问题梳理到了这里,基本上可以进一步聚焦了,在没有大bug的情况下,需要重点看下iptables(netfilter), tc等机制对报文的影响。果然在ECS中有许多iptables规则。利用iptables -nvL可以打出每条rule匹配到的计数,或者利用写log的办法,示例如下: # 记录下new state的报文的日志iptables -A INPUT -p tcp -m state --state NEW -j LOG --log-prefix "[iptables] INPUT NEW: "在这个案例中,通过计数和近一步的log,发现了是OUTPUT chain的最后一跳DROP规则被匹配上了,如下: # iptables -A OUTPUT -m state --state INVALID -j DROP问题的真凶在此时被找到了:iptables规则丢弃了kill进程后发出的FIN包,导致对端收不到,连接无法正常关闭。 到了这里,离最终的root cause还有两个疑问: 问题是否在全局必现?触发的条件是什么?为什么FIN包被认为是INVALID状态?何时触发先来看第一个问题:问题是否在全局必现?触发的条件是什么? 对于ECS上与服务器建立TCP连接的进程,问题实际上不是每次必现的。建议用netcat来做测试,验证下是否是全局影响。通过测试,有如下发现: 利用netcat做类似的操作,也能复现同样的问题,说明这个确实是全局影响,与特定进程或者连接无关。连接时间比较长时能复现,时间比较短时kill进程时能正常发FIN。看下conntrack相关的内核参数设置,发现ECS环境的conntrack参数中有一个显著的调整: net.netfilter.nf_conntrack_tcp_timeout_established = 120这个值默认值是5天,阿里云官网文档推荐的调优值是1200秒,而现在这个ECS环境中的设置是120秒,是一个非常短的值。 看到这里,可以认定是经过nf_conntrack_tcp_timeout_established 120秒后,conntrack中的连接跟踪记录已经被删除,此时对这个连接发起主动的FIN,在netfilter中回被判定成INVALID状态。而客户在iptables filter表的OUTPUT chain中对INVALID连接状态的报文采取的是drop行为,最终导致FIN报文在netfilter filter表OUTPUT chain中被丢弃。 FIN包被认为是INVALID状态?对于一个TCP连接,在conntrack中没有连接跟踪表项,一端FIN掉连接的时候的时候被认为是INVALID状态是很符合逻辑的事情。但是没有发现任何文档清楚地描述这个场景:当用户空间TCP socket仍然存在,但是conntrack表项已经不存在时,对一个“新”的报文,conntrack模块认为它是什么状态。 所有文档描述conntrack的NEW, ESTABLISHED, RELATED, INVALID状态时大同小异,比较详细的描述如文档: The NEW state tells us that the packet is the first packet that we see. This means that the first packet that the conntrack module sees, within a specific connection, will be matched. For example, if we see a SYN packet and it is the first packet in a connection that we see, it will match. However, the packet may as well not be a SYN packet and still be considered NEW. This may lead to certain problems in some instances, but it may also be extremely helpful when we need to pick up lost connections from other firewalls, or when a connection has already timed out, but in reality is not closed.如上对于NEW状态的描述为:conntrack module看见的一个报文就是NEW状态,例如TCP的SYN报文,有时候非SYN也被认为是NEW状态。 ...

June 5, 2019 · 4 min · jiezi

大团队和敏捷开发谁说不可兼得

阿里妹导读:当小团队的产出跟不上业务需要,团队就面临规模化的问题。从1个团队到3个团队,仍可以通过简单的团队沟通保持高效协作。当产品复杂到需要5个以上团队同时开发时,我们需要一定的组织设计来保证团队间的顺畅协作,使得多团队共同开发一个产品时仍能保持敏捷性。这时候的组织该如何设计?今天,我们听听阿里敏捷教练怎么说。1、保持小团队在初创企业或产品刚起步时,团队通常都不大。随着业务的发展,需求越来越多,产品越来越复杂,很多团队的第一反应都是加人。事实上,加人并不是唯一选择,也未必是最优选择。很多时候,小团队能交付惊人的业务成果。 一方面,通过保持专注:Do one thing and do it well,小团队可以聚焦于核心业务,摒除不必要的干扰。有一款微处理器 ARM 比英特尔先做出来,团队的一个leader 说:“回过头来看,当时我们决定做一款微处理器的时候,我认为我做了两个重要的决定。我信任我的团队,并且给了团队两件英特尔和摩托罗拉永远不会提供给他们员工的东西:第一是缺钱,第二是缺人。他们不得不保持简单”。[2] 类似的,创办于2009年的 WhatsApp 于2014年被 Facebook 收购时,公司只有55名员工,全球活跃用户达到4.5亿人,日发送短消息达160亿条。 另一方面,随着开源运动、中台技术和云化技术的发展,很多非核心业务逻辑可以借助外力快速搭建,在业务高速发展的同时,继续保持一支精干的团队。例如,在阿里巴巴研发协同平台“云效”上,二十分钟就可以搭建一套 Spring Boot web application 的持续集成流水线,包含静态代码扫描、单元测试、编译、打包、部署、接口测试。不仅操作方便快捷,还省去了采购机器、部署和管理 build farm 的开销。 2、业务单元特性团队即便努力保持专注并用尽了技术红利,有时业务的发展还是远远超出预期,此时组建多个团队势在必行。 比较理想的选择是按照业务单元来组建特性团队。一个业务单元类似于一家小型创业公司,有自己的长期使命和愿景,有相对清晰的业务边界和盈利模式。人员方面,各业务单元有独立的业务、产品和研发团队。技术方面,各业务单元可以独立完成产品开发的全流程,包括业务决策、产品设计、开发、测试和发布,尽量避免业务单元之间的依赖。 作为一个超级 app,手机淘宝分为几条业务线,同一条业务线内还分为几个独立业务。例如,微淘和淘宝直播都属于内容平台业务线,二者的内容生产、传播渠道、受众和盈利模式不同,因而是相对独立的业务单元。二者有独立的业务、产品和研发团队,业务目标也分开设定和衡量。 技术上解耦是各业务单元能够独立发展的前提。为了解决团队间的依赖,手机淘宝对架构做了容器化改造:一些必要的初始化操作放在 common 容器中,各业务在自己的 bundle 中。各业务 bundle 按需加载,只能依赖底层的 common 架构,不能相互依赖。这样各业务 bundle 可以并行开发,互不干扰。 按照独立的业务边界来组建特性团队,团队能独立发布新功能,迅速获得市场反馈,通过不断试错找到业务发展的方向。 全球第一大音乐平台、音乐流媒体公司 Spotify 也按照业务单元组建团队。 在" Scaling Agile @ Spotify with Tribes, Squads, Chapters & Guilds "[1] ,敏捷教练 Henrik Kniberg 详细介绍了 Spotify 模式。 Spotify 的30多个“小分队”(squad)分布在全球的三个城市,每个 squad 负责产品的特定方向(例如搜索或 radio)。每个 squad 相当于一个小创业公司,squad 没有特定的主管,只有一位产品负责人(Product Owner)。PO 负责业务方向,squad 成员组成跨职能团队交付业务结果。PO 帮助 squad 制定目标和管理优先级,也会定期维护公司层面的产品路线图并确保 squad 的目标与公司战略相匹配。squad被鼓励应用精益创业原则,例如先交付 MVP(minimum viable product),并通过 A/B 测试来验证假设。此外,squad 可以得到敏捷教练的帮助,敏捷教练引导 squad 持续改进并帮助团队移除障碍。 ...

May 21, 2019 · 1 min · jiezi

ES6class模块化在vue中应用10

我们在之前文章《ES6 class与面向对象编程》中,说到了目前大部分框架和库,都采用了面向对象方式编程。那么具体是怎么样应用的呢?面向对象编程,最典型和最基础的作用就是封装,封装的好处就是代码的能够复用,模块化,进行项目和文件的组织。 今天我们就来看看ES6class、模块化在一个被前端人员广泛使用的库-vue中,是怎么应用的。 在说vue模块化之前,我们先说说实现模块化的发展历程,这样才能不仅仅知其然,更知其所以然。 不然你看到vue的一个用法,你看到的只是这个用法,至于为什么是这样做,而不是其他方式,就不清楚了。这也是很多一看就懂,一写就卡的原因。 因为你学到的仅仅是这个例子,没办法迁移到自己的项目中。我们从头捋一捋: js模块化历史 很久很久以前,我们写代码是酱紫的, <script src="a.js"></script> <script src="b.js"></script> <script src="c.js"></script>但是这样写容易出一个问题,也就是变量名冲突,比如a.js 是一个人写的,而b.js是另外一个人写的,两个人用了同样一个变量 var a = 12;var a = 5;这样就会出现变量覆盖的问题,当然我这里不想提听起来高大上的名字,什么全局变量污染。说的就是这点事儿。针对这个问题,最原始最古老的IIFE来了,这是比较简单的创建 JS 模块的方法了。 //a.js(function(){ var a = 12;})();//b.js(function(){ var a = 12;})();这种方式在以前的各种库里面应用很多,比如大名鼎鼎的jquery: (function( window, undefined ) { })(window);但是这种模块化方式有一个缺点,不能解决依赖问题。 比如b.js里面的一个值,必须是a.js里面一个值计算完之后给b.js ,这样才能有正确的结果,显然,IIFE(匿名函数自执行)方式没办法解决。 好吧,我用一句大家听起来可能不太懂的话来显示一下我的专业性: 它只是把变量和方法都封装在本身作用域内的一种普通模式。其存在的缺点就是没有帮我们处理依赖。 说的就是上面的事儿。 然后AMD来了,别误会,不是CPU,是模块化方式,AMD (异步模块依赖) : 其中代表就是Require.js。它很受欢迎,它可以给模块注入依赖,还允许动态地加载 JS 块。 如下: define(‘myModule’, [‘dep1’, ‘dep2’], function (dep1, dep2){ // JavaScript chunk, with a potential deferred loading return {hello: () => console.log(‘hello from myModule’)};});// anywhere elserequire([‘myModule’], function (myModule) { myModule.hello() // display ‘hello form myModule’});有人说我看不懂这个代码,啥意思啊? ...

May 13, 2019 · 2 min · jiezi

ES6class与模块化9

JavaScript语言自创立之初,一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。 很多编程语言都有这项功能,比如 Python的import、Ruby的require,甚至就连CSS都有@import,但是JavaScript没有这方面的支持,这增加了开发大型的、复杂的项目时的难度。 于是前端开发者们开始想办法,为了防止命名空间被污染,采用的是命名空间的方式。 在ES6之前,一些前端社区制定了模块加载方案,最主要的有CommonJS和AMD两种。前者用于服务器,后者用于浏览器。 但这两种规范都由开源社区制定,没有统一,而ES6中引入了模块(Module)体系,从语言层在实现了模块机制,实现了模块功能,而且实现得相当简单,为JavaScript开发大型的、复杂的项目扫清了障碍。 ES6中的模块功能主要由两个命令构成:export和import。 export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能,二者属于相辅相成、一一对应关系。 一、什么是模块 模块可以理解为函数代码块的功能,是封装对象的属性和方法的javascript代码,它可以是某单个文件、变量或者函数。 模块实质上是对业务逻辑分离实现低耦合高内聚,也便于代码管理而不是所有功能代码堆叠在一起,模块真正的魔力所在是仅导出和导入你需要的绑定,而不是将所有的东西都放到一个文件。 在理想状态下我们只需要完成自己部分的核心业务逻辑代码,其他方面的依赖可以通过直接加载被人已经写好模块进行使用即可。 二、export 导出 命令 一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果想从外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。分为以下几种情况: (1)在需要导出的lib.js文件中, 使用 export{接口} 导出接口, 大括号中的接口名字为上面定义的变量, import和引入的main.js文件中的export是对应的: //lib.js 文件let bar = "stringBar";let foo = "stringFoo";let fn0 = function() { console.log("fn0");};let fn1 = function() { console.log("fn1");};export{ bar , foo, fn0, fn1}//main.js文件import {bar,foo, fn0, fn1} from "./lib";console.log(bar+"_"+foo);fn0();fn1();(2)在export接口的时候, 我们可以使用 XX as YY, 把导出的接口名字改了, 比如: xiaoming as haoren, 这样做的目的是为了让接口字段更加语义化。 //lib.js文件let fn0 = function() { console.log("fn0");};let obj0 = {}export { fn0 as foo, obj0 as bar};//main.js文件import {foo, bar} from "./lib";foo();console.log(bar); ...

May 12, 2019 · 2 min · jiezi

npm常用命令与操作

npm版本6.1.0 常用命令与操作1.安装模块npm i/install moduleName安装模块;i是install的缩写,两者功能是一样的npm i moduleName@0.0.1 安装模块的指定版本npm i moduleName --save 安装并保存至package.json文件的dependencies中npm i moduleName --save-dev 安装并保存至package.json文件的devDependencies中npm i moduleName -g 全局安装模块 2.查看已安装模块npm ls 查看所有局部安装的模块npm ls -g 查看所有全局安装的模块npm ls moduleName 查看指定模块的局部安装情况npm ls moduleName -g 查看指定模块的全局安装情况npm view moduleName 查看当前源中指定模块的信息npm view moduleName versions 查看当前源中指定模块的所有历史版本npm view moduleName version 查看当前源中指定模块的最新版本 3.卸载模块npm uninstall moduleName 卸载指定模块 4.更新模块npm update 按照package.json中的描述更新模块,且会在package.json文件中保存更新后的版本描述;^a.b.c更新至a下的最新版本,~a.b.c更新至a.b下的最新版本,a.b.c不会做任何更新npm update moduleName 更新指定模块 5.npm源查看与修改npm config get registry 查看当前npm源地址npm config set registry registryAddress 将npm源设置成相应的地址 6.万能的helpnpm help 当忘记了相应命令后,查看帮助 ...

May 10, 2019 · 1 min · jiezi

深度-API-设计最佳实践的思考

阿里妹导读:API 是模块或者子系统之间交互的接口定义。好的系统架构离不开好的 API 设计,而一个设计不够完善的 API 则注定会导致系统的后续发展和维护非常困难。接下来,阿里巴巴研究员谷朴将给出建议,什么样的 API 设计是好的设计?好的设计该如何做? 作者简介:张瓅玶 (谷朴),阿里巴巴研究员,负责阿里云容器平台集群管理团队。本科和博士毕业于清华大学。 前言API 设计面临的挑战千差万别,很难有处处适用的准则,所以在讨论原则和最佳实践时,无论这些原则和最佳实践是什么,一定有适应的场景和不适应的场景。因此我们在下文中不仅提出一些建议,也尽量去分析这些建议在什么场景下适用,这样我们也可以有针对性地采取例外的策略。 为什么去讨论这些问题? API 是软件系统的核心,而软件系统的复杂度 Complexity 是大规模软件系统能否成功最重要的因素。但复杂度 Complexity 并非某一个单独的问题能完全败坏的,而是在系统设计尤其是API设计层面很多很多小的设计考量一点点叠加起来的(John Ousterhout老爷子说的Complexity is incremental【8】)。 成功的系统不是有一些特别闪光的地方,而是设计时点点滴滴的努力积累起来的。 范围本文偏重于一般性的API设计,并更适用于远程调用(RPC或者HTTP/RESTful的API),但是这里没有特别讨论RESTful API特有的一些问题。 另外,本文在讨论时,假定了客户端直接和远程服务端的API交互。在阿里,由于多种原因,通过客户端的 SDK 来间接访问远程服务的情况更多一些。这里并不讨论 SDK 带来的特殊问题,但是将 SDK 提供的方法看作远程 API 的代理,这里的讨论仍然适用。 API 设计准则:什么是好的 API在这一部分,我们试图总结一些好的 API 应该拥有的特性,或者说是设计的原则。这里我们试图总结更加基础性的原则。所谓基础性的原则,是那些如果我们很好地遵守了就可以让 API 在之后演进的过程中避免多数设计问题的原则。 提供清晰的思维模型 provides a good mental model 为什么这一点重要?因为 API 的设计本身最关键的难题并不是让客户端与服务端软件之间如何交互,而是设计者、维护者、API使用者这几个程序员群体之间在 API 生命周期内的互动。一个 API 如何被使用,以及API本身如何被维护,是依赖于维护者和使用者能够对该 API 有清晰的、一致的认识。这非常依赖于设计者提供了一个清晰易于理解的模型。这种状况实际上是不容易达到的。 就像下图所示,设计者心中有一个模型,而使用者看到和理解的模型可能是另一个模式,这个模式如果比较复杂的话,使用者使用的方式又可能与自己理解的不完全一致。 对于维护者来说,问题是类似的。 而好的 API 让维护者和使用者能够很容易理解到设计时要传达的模型。带来理解、调试、测试、代码扩展和系统维护性的提升 。 图片来源:https://medium.com/@copyconstruct/effective-mental-models-for-code-and-systems-7c55918f1b3e ...

May 9, 2019 · 4 min · jiezi

Sentinel-成为-Spring-Cloud-官方推荐的主流熔断降级方案

近日,Sentinel 贡献的 spring-cloud-circuitbreaker-sentinel  模块正式被Spring Cloud社区合并至 Spring Cloud Circuit Breaker,由此,Sentinel 加入了 Spring Cloud Circuit Breaker 俱乐部,成为 Spring Cloud 官方的主流推荐选择之一。这意味着,Spring Cloud 微服务的开发者在熔断降级领域有了更多的选择,可以更方便地利用 Sentinel 来保障微服务的稳定性。 一、什么是 Spring Cloud Circuit Breaker?Spring Cloud Circuit Breaker是 Spring Cloud 官方的熔断器组件库,提供了一套统一的熔断器抽象API接口,允许开发者自由选择合适的熔断器实现。这个官方的熔断器组件库,截至目前,官方推荐的熔断器组件有: HystrixResilience4JSentinelSpring Retry当前,Spring Cloud Circuit Breaker 处于孵化阶段中,未来将合并到 Spring Cloud 主干版本正式发布。 Spring Cloud Circuit Breaker https://github.com/spring-cloud-incubator/spring-cloud-circuitbreaker 二、Sentinel 发展历程2012 年,Sentinel 诞生于阿里巴巴集团内部,主要功能为入口流量控制; 2013 - 2018 年,Sentinel 在阿里巴巴集团内部迅速发展,成为基础技术模块,覆盖了所有的核心场景。Sentinel 也因此积累了大量的流量控制场景以及生产实践; 2018年7月,阿里巴巴宣布限流降级框架组件 Sentinel 正式开源,在此之前,Sentinel 作为阿里巴巴“大中台、小前台”架构中的基础模块,已经覆盖了阿里的所有核心场景,因此积累了大量的流量归整场景以及生产实践; 2018年9月,Sentinel 发布 v0.2.0版本,释放异步调用支持、热点参数限流等多个重要特性; 2018年10月,Sentinel 发布首个 GA 版本 v1.3.0,该版本包括 Sentinel 控制台功能的完善和一些 bug 修复,以及其它的产品改进; ...

April 29, 2019 · 1 min · jiezi

【Node】CommonJS 包规范与 NPM 包管理

NPM 实践了 CommonJS 包规范规范,帮助我们安装和管理依赖包,使得 Node 项目的第三方模块更加规范便捷,可以在 NPM 平台上找到所有共享的插件。一、CommonJS 包规范CommonJS 包规范的定义分为两部分:用于组织文件目录的包结构和用于描述包信息的包描述文件 package.json。1.1 包结构一个包由相当于一个存档文件,可压缩为.zip 或tar.gz,安装时解压还原。完全符合 CommonJS 包规范的目录包含以下文件:|– .bin // 存放可执行的二进制文件|– lib // 存放 Javascript 文件|– doc // 存放文档|– test // 存放单元测试用例|– package.json // 包描述文件1.2 包描述文件包描述文件 package.json 是一个 JSON 文件,在包的根目录下。NPM 的所有行为都与 package.json 中的字段有关,Node 程序的依赖项也体现在这些字段上。CommonJS 包规范定义了 package.json 中的字段,NPM 实现时对 CommonJS 包规范中的字段也进行取舍和新增,常用字段有:name: 包名称;description: 包描述;keyword: 关键字数组,用于 NPM 分类搜索;repository: 代码托管位置列表;homepage: 当前包的网址;bugs: 反馈 bug 的邮箱或网址;dependencies: 使用当前包所需要的依赖包列表,NPM 根据这个属性自动加载依赖;devDependencies: 后续开发时需要安装的依赖包列表;main: 模块入口,使用require()引入时优先检查该字段。如果 main 字段不存在,Node 按照模块文件定位的规则依次查找包目录下的 index.js、index.node、index.json;scripts: 包管理器用来安装、编译、测试包的命令对象。bin: 配置包的 bin 字段后,可以通过npm install package_name -g将包添加到执行路径中,之后可以“全局使用”。二、npm 管理NPM 帮助 Node 完成第三方模块的发布、安装和依赖。可以直接执行$ npm 查看所有命令。使用$ npm init 可以快速生成一个 package.json 文件。2.1 npm install 原理使用 npm install安装依赖包是 NPM 最常用的功能,例如执行npm install express后,npm 向 registry 查询模块压缩包的网址,下载压缩包后 NPM 会在当前的 node_module 目录下创建 express 目录,将包解压还原在此。registry 是 NPM 模块仓库提供了一个查询服务,例如 npmjs.org 的查询服务网址 https://registry.npmjs.org/ ,加模块名 https://registry.npmjs.org/vue 就得到包含 Vue 模块所有版本的信息 JSON 对象,也可以使用$ npm view vue查询。Node 项目使用require(’express’)引入 express 模块时,require()方法在路径分析时按照模块路径查找策略,沿当前路径向上逐级查找node_module目录,最终定位到 express 目录。包的安装和模块引入是相辅相成的, 可以进一步理解 Node 模块加载原理2.2 npm install 使用npm install默认将包和 package.json 的依赖关系保存在dependencies,但在可以通过一些额外的标志来控制它们的保存位置和方式:-P or –save or –save-prod: 依赖在dependencies,默认值.-D or –save-dev: 依赖在 devDependencies.-O or –save-optional: 依赖在 optionalDependencies.–no-save: 防止包依赖保存在 dependencies.例如 npm install express -D 就会将 express 依赖关系保存在devDependencies。在npm install一个模块时经常纠结要安装在devDependencies还是dependencies,从字面意思看前者用于生产环境,后者用于开发环境。在官方的定义中,如果环境变量 NODE_ENV 设置为 production,执行 npm install –production 时 npm 会默认安装dependencies里面的依赖项,不会去安装devDependencies里的。并且推荐dependencies里配置正式运行时必须依赖的插件,devDependencies通常用来放我们开发或测试的工具,比如 Webpack,Gulp,babel,eslint等。在实际开发过程中,Node 包的安装是依据 require/import 模块机制,无论是-P还是-D指令都会把依赖下载到 node_modules 文件夹,-P还是-D只是修改了dependencies对象,在安装这个库进行开发调试的时候,可以通过npm install一键安装这两个目录下所有的依赖。2.3 全局安装使用 -g或 –global可以将包安装为“全局可用”,但需要注意的是,全局安装并不意味将模块包安装为一个全局包,也不是可以在任何地方都可以require()引入。实际上-g命令是将模块包安装在“全局”的node_module中,即 Node 可执行文件相同的路径下,并通过配置 bin 字段链接。例如使用命令行查看 Node 可执行文件的位置:$ which node/usr/local/bin/node那么全局安装模块的实际位置就是/usr/local/lib/node_modules(在 Finder 中用 command+shift+G 快捷键访问隐藏目录 )进一步了解 NPM 的使用可以看 NPM DOCS,NPM更多命令 NPM CLI继续加油哦永远十八岁的少女~ ...

April 16, 2019 · 1 min · jiezi

前端工程师必备:前端的模块化

JS模块化模块化的理解什么是模块?将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起;块的内部数据/实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信;一个模块的组成数据—>内部的属性;操作数据的行为—>内部的函数;模块化是指解决一个复杂的问题时自顶向下把系统划分成若干模块的过程,有多种属性,分别反映其内部特性;模块化编码:编码时是按照模块一个一个编码的, 整个项目就是一个模块化的项目;非模块化的问题页面加载多个js的问题:<script type=“text/javascript” src=“module1.js”></script><script type=“text/javascript” src=“module2.js”></script><script type=“text/javascript” src=“module3.js”></script><script type=“text/javascript” src=“module4.js”></script>发生问题:难以维护 ;依赖模糊;请求过多;所以,这些问题可以通过现代模块化编码和项目构建来解决;模块化的优点更好地分离:避免一个页面中放置多个script标签,而只需加载一个需要的整体模块即可,这样对于HTML和JavaScript分离很有好处;更好的代码组织方式:有利于后期更好的维护代码;按需加载:提高使用性能,和下载速度,按需求加载需要的模块避免命名冲突:JavaScript本身是没有命名空间,经常会有命名冲突,模块化就能使模块内的任何形式的命名都不会再和其他模块有冲突。更好的依赖处理:使用模块化,只需要在模块内部申明好依赖的就行,增加删除都直接修改模块即可,在调用的时候也不用管该模块依赖了哪些其他模块。模块化的发展历程原始写法只是把不同的函数简单地放在一起,就算一个模块;function fun1(){ //…}function fun2(){ //…}//上面的函数fun1,fun2组成了一个模块,使用的时候直接调用某个函数就行了。缺点:“污染"了全局变量,无法保证不与其他模块发生变量名冲突;模块成员之间看不出直接关系。对象写法为了解决污染全局变量的问题,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。 var module1 = new Object({ count : 0, fun1 : function (){ //… }, fun2 : function (){ //… } }); //这个里面的fun1和fun2都封装在一个赌侠宁里,可以通过对象.方法的形式进行调用; module1.fun1();优点:减少了全局上的变量数目;缺点:本质是对象,而这个对象会暴露所有模块成员,内部状态可以被外部改写。立即执行函数(IIFE模式)避免暴露私有成员,所以使用立即执行函数(自调函数,IIFE);作用: 数据是私有的, 外部只能通过暴露的方法操作var module1 = (function(){ var count = 0; var fun1 = function(){ //… } var fun2 = function(){ //… } //将想要暴露的内容放置到一个对象中,通过return返回到全局作用域。 return{ fun1:fun1, fun2:fun2 }})()//这样的话只能在全局作用域中读到fun1和fun2,但是读不到变量count,也修改不了了。//问题:当前这个模块依赖另一个模块怎么办?IIFE的增强(引入依赖)如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"增强模式”;IIFE模式增强:引入依赖;这就是现代模块实现的基石;var module1 = (function (mod){ mod.fun3 = function () { //… }; return mod;})(module1);//为module1模块添加了一个新方法fun3(),然后返回新的module1模块。//引入jquery到项目中;var Module = (function($){ var $body = $(“body”); // we can use jQuery now! var foo = function(){ console.log($body); // 特权方法 } // Revelation Pattern return { foo: foo }})(jQuery)Module.foo();js模块化需要解决那些问题:1.如何安全的包装一个模块的代码?(不污染模块外的任何代码)2.如何唯一标识一个模块?3.如何优雅的把模块的API暴漏出去?(不能增加全局变量)4.如何方便的使用所依赖的模块?模块化规范Node: 服务器端Browserify : 浏览器端CommonJS:服务器端概述Node 应用由模块组成,采用 CommonJS 模块规范。CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。特点所有代码都运行在模块作用域,不会污染全局作用域。模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。模块加载的顺序,按照其在代码中出现的顺序。基本语法:定义暴露模块 : exportsexports.xxx = value// 通过module.exports指定暴露的对象valuemodule.exports = value引入模块 : requirevar module = require(‘模块相对路径’)引入模块发生在什么时候?Node:运行时, 动态同步引入;Browserify:在运行前对模块进行编译/转译/打包的处理(已经将依赖的模块包含进来了), 运行的是打包生成的js, 运行时不需要再从远程引入依赖模块;CommonJS通用的模块规范(同步)Node内部提供一个Module构建函数。所有模块都是Module的实例。每个模块内部,都有一个module对象,代表当前模块。module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。Node为每个模块提供一个exports变量,指向module.exports。如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。Modules/1.0规范包含内容:模块的标识应遵循的规则(书写规范)定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为模块暴露出来的API;如果被require函数引入的模块中也包含依赖,那么依次加载这些依赖;如果引入模块失败,那么require函数应该报一个异常;模块通过变量exports来向外暴露API,exports赋值暴露的只能是一个对象exports = {Obj},暴露的API须作为此对象的属性。exports本质是引入了module.exports的对象。不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。如果暴露的不是变量exports,而是module.exports。module变量代表当前模块,这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。exports=module.exports={Obj}node中的commonJS教程1.安装node.js;2.创建项目结构//结构如下|-modules |-module1.js//待引入模块1 |-module2.js//待引入模块2 |-module3.js//待引入模块3|-app.js//主模块|-package.json { “name”: “commonjsnode”, “version”: “1.0.0” }3.下载第三方模块:举例expressnpm i express –save4.模块化编码// module1 // 使用module.exports = value向外暴露一个对象module.exports = { name: ’this is module1’, foo(){ console.log(‘module1 foo()’); }}// module2 // 使用module.exports = value向外暴露一个函数 module.exports = function () { console.log(‘module2()’);}// module3 // 使用exports.xxx = value向外暴露一个对象 exports.foo = function () { console.log(‘module3 foo()’); }; exports.bar = function () { console.log(‘module3 bar()’); }; exports.name = ’this is module3’//app.js文件var uniq = require(‘uniq’);//引用模块let module1 = require(’./modules/module1’);let module2 = require(’./modules/module2’);let module3 = require(’./modules/module3’);//使用模块module1.foo();module2();module3.foo();module3.bar();module3.name;5.通过node运行app.js命令:node.app.js工具:右键–>运行浏览器中的commonJS教程借助Browserify步骤创建项目结构|-js |-dist //打包生成文件的目录 |-src //源码所在的目录 |-module1.js |-module2.js |-module3.js |-app.js //应用主源文件|-index.html //浏览器上的页面|-package.json { “name”: “browserify-test”, “version”: “1.0.0” }下载browserify全局: npm install browserify -g局部: npm install browserify –save-dev定义模块代码:index.html文件要运行在浏览器上,需要借助browserify将app.js文件打包编译,如果直接在index.html引入app.js就会报错。打包处理js:根目录下运行browserify js/src/app.js -o js/dist/bundle.js页面使用引入:<script type=“text/javascript” src=“js/dist/bundle.js”></script> AMD : 浏览器端CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数,可以实现异步加载依赖模块,并且会提前加载;由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。语法AMD规范基本语法定义暴露模块: define([依赖模块名], function(){return 模块对象})引入模块: require([‘模块1’, ‘模块2’, ‘模块3’], function(m1, m2){//使用模块对象})兼容CommonJS规范的输出模块define(function (require, exports, module) { var reqModule = require("./someModule"); requModule.test(); exports.asplode = function () { //someing }}); AMD:异步模块定义规范(预加载)AMD规范:https://github.com/amdjs/amdj…AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:require([module], callback);第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。目前,主要有两个Javascript库实现了AMD规范:RequireJS和curl.js。RequireJS优点实现js文件的异步加载,避免网页失去响应;管理模块之间的依赖性,便于代码的编写和维护。require.js使用教程下载require.js, 并引入官网: https://requirejs.org/中文:https://blog.csdn.net/sanxian…github : https://github.com/requirejs/…将require.js导入项目: js/libs/require.js创建项目结构|-js |-libs |-require.js // 引入的require.js |-modules |-alerter.js |-dataService.js |-main.js|-index.html定义require.js的模块代码require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。具体来说,就是模块必须采用特定的define()函数来定义;如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。define([‘myLib’], function(myLib){ function foo(){ myLib.doSomething(); } // 暴露模块 return {foo : foo};});//当require()函数加载上面这个模块的时候,就会先加载myLib.js文件。 - 如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性; // dataService.js define(function () { let msg = 'this is dataService' function getMsg() { return msg.toUpperCase() } return {getMsg} }) // alerter.js define(['dataService', 'jquery'], function (dataService, $) { let name = 'Tom2' function showMsg() { $('body').css('background', 'gray') alert(dataService.getMsg() + ', ' + name) } return {showMsg} }) 应用主(入口)js: main.js使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块main.js的头部,参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。 (function () { //配置 require.config({ //基本路径 baseUrl: “js/”, //模块标识名与模块路径映射 paths: { “alerter”: “modules/alerter”,//此处不能写成alerter.js,会报错 “dataService”: “modules/dataService”, } }) //引入使用模块 require( [‘alerter’], function(alerter) { alerter.showMsg() }) })()页面使用模块:<script data-main=“js/main” src=“js/libs/require.js”></script>定义模块require.config()接受一个配置对象,这个对象除了有前面说过的paths属性之外,还有一个shim属性,专门用来配置不兼容的模块。具体来说,每个模块要定义:1、exports值(输出的变量名),表明这个模块外部调用时的名称;2、deps数组,表明该模块的依赖性。支持的配置项:baseUrl :所有模块的查找根路径。当加载纯.js文件(依赖字串以/开头,或者以.js结尾,或者含有协议),不会使用baseUrl。如未显式设置baseUrl,则默认值是加载require.js的HTML所处的位置。如果用了data-main属性,则该路径就变成baseUrl。baseUrl可跟require.js页面处于不同的域下,RequireJS脚本的加载是跨域的。唯一的限制是使用text! plugins加载文本内容时,这些路径应跟页面同域,至少在开发时应这样。优化工具会将text! plugin资源内联,因此在使用优化工具之后你可以使用跨域引用text! plugin资源的那些资源。paths:path映射那些不直接放置于baseUrl下的模块名。设置path时起始位置是相对于baseUrl的,除非该path设置以"/“开头或含有URL协议(如http:)。用于模块名的path不应含有.js后缀,因为一个path有可能映射到一个目录。路径解析机制会自动在映射模块名到path时添加上.js后缀。在文本模版之类的场景中使用require.toUrl()时它也会添加合适的后缀。在浏览器中运行时,可指定路径的备选(fallbacks),以实现诸如首先指定了从CDN中加载,一旦CDN加载失败则从本地位置中加载这类的机制;shim: 为那些没有使用define()来声明依赖关系、设置模块的"浏览器全局变量注入"型脚本做依赖和导出配置。使用第三方基于require.js的框架(jquery)将jquery的库文件导入到项目: js/libs/jquery-1.10.1.js在main.js中配置jquery路径paths: { ‘jquery’: ’libs/jquery-1.10.1’}在alerter.js中使用jquerydefine([‘dataService’, ‘jquery’], function (dataService, $) { var name = ‘xfzhang’ function showMsg() { $(‘body’).css({background : ‘red’}) alert(name + ’ ‘+dataService.getMsg()) } return {showMsg} })使用第三方不基于require.js的框架(angular)将angular.js导入项目:js/libs/angular.js流行的函数库(比如jQuery)符合AMD规范,更多的库并不符合。这样的模块在用require()加载之前,要先用require.config()方法,定义它们的一些特征。// main.js中配置(function () { //配置 require.config({ //基本路径 baseUrl: “js/”, //模块标识名与模块路径映射 paths: { //第三方库作为模块 ‘jquery’ : ‘./libs/jquery-1.10.1’, ‘angular’ : ‘./libs/angular’, //自定义模块 “alerter”: “./modules/alerter”, “dataService”: “./modules/dataService” }, /* 配置不兼容AMD的模块 exports : 指定与相对应的模块名对应的模块对象 */ shim: { ‘angular’ : { exports : ‘angular’ } } }) //引入使用模块 require( [‘alerter’, ‘angular’], function(alerter, angular) { alerter.showMsg() console.log(angular); })})()CMD : 浏览器端CMD规范:https://github.com/seajs/seaj…CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范基本语法定义暴露模块:// 没有依赖的模块define(function(require, module, exports){ let value = ‘xxx’; //通过require引入依赖模块 //通过module.exports/exports来暴露模块 exports.xxx = value module.exports = value})// 有依赖的模块define(function(require, exports, module){ //引入依赖模块(同步) var module2 = require(’./module2’) //引入依赖模块(异步) require.async(’./module3’, function (m3) { …… }) //暴露模块 exports.xxx = value})使用模块seajs.use([‘模块1’, ‘模块2’])sea.js简单使用教程下载sea.js, 并引入官网: http://seajs.org/github : https://github.com/seajs/seajs将sea.js导入项目: js/libs/sea.js如何定义导出模块 :define()exportsmodule.exports如何依赖模块:require()如何使用模块: seajs.use()创建项目结构|-js |-libs |-sea.js |-modules |-module1.js |-module2.js |-module3.js |-module4.js |-main.js|-index.html定义sea.js的模块代码module1.jsdefine(function (require, exports, module) { //内部变量数据 var data = ’this is module1’ //内部函数 function show() { console.log(‘module1 show() ’ + data) } //向外暴露 exports.show = show})module2.jsdefine(function (require, exports, module) { module.exports = { msg: ‘I Will Back’ }})module3.jsdefine(function (require, exports, module) { const API_KEY = ‘abc123’ exports.API_KEY = API_KEY})module4.jsdefine(function (require, exports, module) { //引入依赖模块(同步) var module2 = require(’./module2’); function show() { console.log(‘module4 show() ’ + module2.msg) } exports.show = show //引入依赖模块(异步) require.async(’./module3’, function (m3) { console.log(‘异步引入依赖模块3 ’ + m3.API_KEY) })})main.js : 主(入口)模块define(function (require) { var m1 = require(’./module1’) var m4 = require(’./module4’) m1.show() m4.show()})index.html:<script type=“text/javascript” src=“js/libs/sea.js”></script><script type=“text/javascript”> seajs.use(’./js/modules/main’)</script>ES6模块化模块化的规范:CommonJS和AMD两种。前者用于服务器,后者用于浏览器。而ES6 中提供了简单的模块系统,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。基本用法es6 中新增了两个命令 export 和 import ;export 命令用于规定模块的对外接口;import 命令用于输入其他模块提供的功能。一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个JS文件,里面使用export命令输出变量。// math.jsexport const add = function (a, b) { return a + b}export const subtract = function (a, b) { return a - b}使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块(文件)。// main.jsimport { add, subtract } from ‘./math.js’add(1, 2)substract(3, 2)定义暴露模块 : export暴露一个对象:默认暴露,暴露任意数据类型,暴露什么数据类型,接收什么数据类型export default 对象暴露多个:常规暴露,暴露的本质是对象,接收的时候只能以对象的解构赋值的方式来接收值export var xxx = value1export let yyy = value2// 暴露一个对象var xxx = value1let yyy = value2export {xxx, yyy}引入使用模块 : importdefault模块:import xxx from ‘模块路径/模块名’其它模块import {xxx, yyy} from ‘模块路径/模块名’import * as module1 from ‘模块路径/模块名’export 详细用法export不止可以导出函数,还可以导出,对象、类、字符串等等;暴露多个:分别暴露export const obj = {test1: ‘’}export const test = ‘’export class Test { constuctor() { }}// 或者,直接在暴露的地方定义导出函数或者变量export let foo = ()=>{console.log(‘fnFoo’);return “foo”},bar=“stringBar"一起暴露,推荐使用这种写法,这样可以写在脚本尾部,一眼就看清楚输出了哪些变量。let a=1let b=2let c=3export { a,b,c }还可以通过as改变输出名称// test.jslet a = 1let b = 2let c = 3export { a as test, b, c};import { test, b, c } from ‘./test.js’ // 改变命名后只能写 as 后的命名通过通配符暴露其他引入的模块// test.jslet a = 1let b = 2let c = 3export { a as test, b, c};// lib.js引入test.js的内容export * from ‘./test.js’// 引入import {test,b,c} from ‘./lib.js’暴露一个对象,默认暴露export default指定默认输出,import无需知道变量名就可以直接使用// test.jsexport default function () { console.log(‘hello world’)}//引入import say from ‘./test.js’ // 这里可以指定任意变量名say() // hello world常用的模块import $ from ‘jQuery’ // 加载jQuery 库import _ from ’lodash’ // 加载 lodashimport moment from ‘moment’ // 加载 momentimport详细用法import 为加载模块的命令,基础使用方式和之前一样// main.jsimport { add, subtract } from ‘./test’// 对于export default 导出的import say from ‘./test’通过 as 命令修改导入的变量名import {add as sum, subtract} from ‘./test’sum (1, 2)加载模块的全部,除了指定输出变量名或者 export.default 定义的导入, 还可以通过 * 号加载模块的全部。// math.jsexport const add = function (a, b) { return a + b}export const subtract = function (a, b) { return a - b}//引入import * as math from ‘./test.js’math.add(1, 2)math.subtract(1, 2)ES6-Babel-Browserify使用教程问题: 所有浏览器还不能直接识别ES6模块化的语法解决:使用Babel将ES6—>ES5(使用了CommonJS) —-浏览器还不能直接执行;使用Browserify—>打包处理js—-浏览器可以运行定义package.json文件{ “name” : “es6-babel-browserify”, “version” : “1.0.0”}安装babel-cli, babel-preset-es2015和browserifynpm install babel-cli browserify -gnpm install babel-preset-es2015 –save-dev 定义.babelrc文件,这是一个babel的设置文件{ “presets”: [“es2015”] }编码// js/src/module1.jsexport function foo() { console.log(‘module1 foo()’);};export let bar = function () { console.log(‘module1 bar()’);};export const DATA_ARR = [1, 3, 5, 1];// js/src/module2.jslet data = ‘module2 data’; function fun1() { console.log(‘module2 fun1() ’ + data);};function fun2() { console.log(‘module2 fun2() ’ + data);};export {fun1, fun2};// js/src/module3.jsexport default { name: ‘Tom’, setName: function (name) { this.name = name }}// js/src/app.jsimport {foo, bar} from ‘./module1’import {DATA_ARR} from ‘./module1’import {fun1, fun2} from ‘./module2’import person from ‘./module3’import $ from ‘jquery’//引入完毕$(‘body’).css(‘background’, ‘red’)foo()bar()console.log(DATA_ARR);fun1()fun2()person.setName(‘JACK’)console.log(person.name);编译使用Babel将ES6编译为ES5代码(但包含CommonJS语法) : babel js/src -d js/lib使用Browserify编译js : browserify js/lib/app.js -o js/lib/bundle.js页面中引入测试<script type=“text/javascript” src=“js/lib/bundle.js”></script>引入第三方模块(jQuery)1). 下载jQuery模块:npm install jquery@1 –save- 2). 在app.js中引入并使用import $ from 'jquery'$('body').css('background', 'red')总结模块化方案优点缺点commonJS复用性强; 使用简单;实现简单;有不少可以拿来即用的模块,生态不错;同步加载不适合浏览器,浏览器的请求都是异步加载;不能并行加载多个模块。AMD异步加载适合浏览器可并行加载多个模块;模块定义方式不优雅,不符合标准模块化ES6可静态分析,提前编译面向未来的标准;浏览器原生兼容性差,所以一般都编译成ES5;目前可以拿来即用的模块少,生态差AMD和CMD区别:权威参考:https://github.com/seajs/seaj…对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.CMD 推崇依赖就近,AMD 推崇依赖前置。// CMDdefine(function(require, exports, module) { var a = require(’./a’); a.doSomething() // 此处略去 100 行 var b = require(’./b’) // 依赖可以就近书写 b.doSomething() // … })// AMD 默认推荐的是define([’./a’, ‘./b’], function(a, b) { // 依赖必须一开始就写好 a.doSomething() // 此处略去 100 行 b.doSomething() …})虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。还有一些细节差异,具体看这个规范的定义就好,就不多说了。参考:使用 AMD、CommonJS 及 ES Harmony 编写模块化的 JavaScript ...

April 12, 2019 · 5 min · jiezi

原来,阿里工程师才是隐藏的“修图高手”!

摘要: 近些年,深度学习飞速发展,在很多领域(图像、语音、自然语言处理、推荐搜素等)展现出了巨大的优势。多模态表征研究也进行入深度学习时代,各种模态融合策略层出不穷。阿里妹导读:在现实世界中,信息通常以不同的模态同时出现。这里提到的模态主要指信息的来源或者形式。例如在淘宝场景中,每个商品通常包含标题、商品短视频、主图、附图、各种商品属性(类目,价格,销量,评价信息等)、详情描述等,这里的每一个维度的信息就代表了一个模态。如何将所有模态的信息进行融合,进而获得一个综合的特征表示,这就是多模态表征要解决的问题。今天,我们就来探索多模态表征感知网络,了解这项拿过冠军的技术。作者 | 越丰、箫疯、裕宏、华棠摘要近些年,深度学习飞速发展,在很多领域(图像、语音、自然语言处理、推荐搜素等)展现出了巨大的优势。多模态表征研究也进行入深度学习时代,各种模态融合策略层出不穷。在这里,我们主要对图像和文本这两个最常见的模型融合进行探索,并在2个多模态融合场景中取得了目前最好的效果。在文本编辑图像场景中,我们提出了双线性残差层 ( Bilinear Residual Layer ),对图像和文本两个模态的特征进行双线性表示 ( Bilinear Representation),用来自动学习图像特征和文本特征间更优的融合方式。在时尚图像生成场景中(给定文本直接生成对应的图像),我们采用了跨模态注意力机制(Cross Attention)对生成的图像和文本特征进行融合,再生成高清晰度且符合文本描述的时尚图像。最后,在客观评分和主观评分上取得了最好的成绩。文本编辑图像图像编辑是指对模拟图像内容的改动或者修饰,使之满足我们的需要,常见的图像处理软件有Photoshop、ImageReady等。随着人们对于图像编辑需求的日益提升,越来越多的图像要经过类似的后处理。但是图像处理软件使用复杂且需要经过专业的培训,这导致图像编辑流程消耗了大量人力以及时间成本,为解决该问题,一种基于文本的图像编辑手段被提出。基于文本的图像编辑方法通过一段文本描述,自动地编辑源图像使其符合给出的文本描述,从而简化图像编辑流程。例如图1所示,通过基于文本的图像编辑技术可以通过文字命令改变模特衣服的颜色,纹理甚至款式。然而,基于文本的图像编辑技术目前仍然难以实现,原因是文本和图像是跨模态的,要实现一个智能的图像编辑系统则需要同时提取文本和源图像中的关键语义。这使得我们的模型需要很强的表示学习能力。现有方法目前已有一些针对基于文本的图像编辑所提出的方法。他们都采用了强大的图像生成模型GAN(Generative adversarial network)作为基本框架。Hao[1]训练了一个conditional GAN,它将提取出来的text embeddings作为conditional vector和图像特征连接在一起,作为两个模态信息的混合表示,然后通过反卷积操作生成目标图像 (如图2)。Mehmet[2]对以上方法做了改进,他认为特征连接并不是一种好的模态信息融合方式,并用一种可学习参数的特征线性调制方法3去学习图像和文本的联合特征。FiLM减少了模型的参数,同时使得联合特征是可学习的,提高了模型的表示学习能力 (如图3)。我们的工作我们的工作从理论角度分析了连接操作和特征线性调制操作间特征表示能力的优劣,并将这两种方法推广到更一般的形式:双线性 (Bilinear representation)。据此,我们提出表示学习能力更加优越的双线性残差层 (Bilinear Residual Layer),用来自动学习图像特征和文本特征间更优的融合方式。Conditioning的原始形式FiLM形式FiLM源自于将特征乘以0-1之间的向量来模拟注意力机制的想法,FiLM进行特征维度上的仿射变换,即:Bilinear形式经过证明,Bilinear形式可以看做FiLM的进一步推广,它具有更加强大的表示学习能力。证明如下:以上形式等同于:Bilinear的Low-rank简化形式实验结果我们的方法在Caltech-200 bird[5]、Oxford-102 flower[6]以及Fashion Synthesis[7]三个数据集上进行了验证。定性结果如图5所示,第一列为原图,第二列表示Conditional GAN原始形式的方法,第三列表示基于FiLM的方法,最后一列是论文提出的方法。很明显前两者对于复杂图像的编辑会失败,而论文提出的方法得到的图像质量都较高。除此之外,实验还进行了定量分析,尽管对于图像生成任务还很难定量评估,但是本工作采用了近期提出的近似评价指标Inception Score (IS)[8]作为度量标准。由表6可见,我们的方法获得了更高的IS得分,同时在矩阵秩设定为256时,IS得分最高。时尚图像生成在调研多模态融合技术的时候,有一个难点就是文本的描述其实对应到图像上局部区域的特性。例如图7,Long sleeve对应了图像中衣服袖子的区域,并且是长袖。另外,整个文本描述的特性对应的是整个图像的区域。基于这个考虑,我们认为图像和文本需要全局和局部特征描述,图像全局特征描述对应到整个图像的特征,局部特征对应图像每个区域的特征。文本的全局特征对应整个句子的特征,文本的局部特征对应每个单词的特征。然后文本和图像的全局和局部区域进行特征融合。针对这种融合策略,我们在时尚图像生成任务上进行了实验。时尚图像生成(FashionGEN)是第一届Workshop On Computer VisionFor Fashion, Art And Design中一个比赛,这个比赛的任务是通过文本的描述生成高清晰度且符合文本描述的商品图像。我们在这个比赛中客观评分和人工评分上均获得的第一,并取得了这个比赛的冠军。我们的方法我们方法基于细粒度的跨模态注意力,主要思路是将不同模态的数据(文本、图像)映射到同一特征空间中计算相似度,从而学习文本中每个单词语义和图像局部区域特征的对应关系,辅助生成符合文本描述的细粒度时尚图像,如图7所示。传统的基于文本的图像生成方法通常只学习句子和图像整体的语义关联,缺乏对服装细节纹理或设计的建模。为了改进这一问题,我们引入了跨模态注意力机制。如图8左边区域,已知图像的局部特征,可以计算句子中不同单词对区域特征的重要性,而句子语义可以视为基于重要性权重的动态表示。跨模态注意力可以将图片与文字的语义关联在更加精细的局部特征层级上建模,有益于细粒度时尚图像的生成。我们用bi-LSTM作为文本编码器,GAN作为对抗生成模型,并将生成过程分为由粗到精,逐步增加分辨率的两个阶段:第一阶段利用句子的整体语义和随机输入学习图像在大尺度上的整体结构。第二阶段利用单词层级的语义在第一阶段低分辨率输出上做局部细节的修正和渲染,得到细粒度的高分辨率时尚图像输出。对抗生成网络传统的生成式对抗网络由判别器和生成器两部分组成,判别器的目标是判别生成图像是否在真实数据集的分布中,而生成器的目标是尽可能的骗过判别器生成逼近真实数据集的图像,通过两者的迭代更新,最终达到理论上的纳什均衡点。这个过程被称为对抗训练,对抗训练的提出为建立图像等复杂数据分布建立了可能性。基于跨模态注意力的相似性图像-文本相似性对于第i个单词,我们最终可以建立不同区域特征的加权和(越相似赋予越大的权重):M为batchsize的大小。文本-图像相似性同理的,文本-图像的相似性可以形式化为:全局相似性通过优化以上损失函数,我们最终得到的生成的服装图片的效果图如下所示:附上算法效果图:总结我们主要对图像和文本这两个最常见的模型融合进行探索,在文本编辑图像任务上,我们提出基于双线性残差层 (Bilinear Residual Layer)的图文融合策略,并取得了最好的效果,相关工作已经发表在ICASSP 2019上,点击文末“阅读原文”即可查看论文。在时尚图像生成任务上,我们使用了细粒度的跨模态融合策略,并在FashionGen竞赛中取得第一。关于我们阿里安全图灵实验室专注于AI在安全和平台治理领域的应用,涵盖风控、知识产权、智能云服务和新零售等商业场景,以及医疗、教育、出行等数亿用户相关的生活场景,已申请专利上百项。2018年12月,阿里安全图灵实验室正式对外推出“安全AI”,并总结其在知识产权保护、新零售、内容安全等领域进行深度应用的成果:2018年全年,内容安全AI调用量达到1.5万亿次;知识产权AI正在为上千个原创商家的3000多个原创商品提供电子“出生证”——线上与全平台商品图片对比,智能化完成原创性校验,作为原创商家电子备案及后续维权的重要依据;新零售场景的防盗损对小偷等识别精准度达到100%。本文作者:越丰阅读原文本文来自云栖社区合作伙伴“ 阿里技术”,如需转载请联系原作者。

April 10, 2019 · 1 min · jiezi

Pick!闲鱼亿级商品库中的秒级实时选品

一、业务背景在电商运营工作中,营销活动是非常重要的部分,对用户增长和GMV都有很大帮助。对电商运营来说,如何从庞大的商品库中筛选出卖家优质商品并推送给有需要的买家购买是每时每刻都要思索的问题,而且这个过程需要尽可能快和实时。保证快和实时就可以提升买卖双方的用户体验,提高用户粘性。二、实时选品为了解决上面提到的问题,闲鱼研发了马赫系统。马赫是一个实时高性能的商品选品系统,解决在亿级别商品中通过规则筛选优质商品并进行投放的场景。有了马赫系统之后,闲鱼的运营同学可以在马赫系统上创建筛选规则,比如商品标题包含“小猪佩奇”、类目为“玩具”、价格不超过100元且商品状态为未卖出。在运营创建规则后,马赫系统会同时进行两步操作,第一步是从存量商品数据筛选符合条件的商品进行打标;第二步是对商品实时变更进行规则计算,实时同步规则命中结果。马赫系统最大的特点是快而实时,体现在命中规模为100w的规则可以在10分钟之内完成打标;商品本身变更导致的规则命中结果同步时间为1秒钟。运营可以通过马赫系统快速筛选商品向用户投放,闲鱼的流量也可以精准投给符合条件的商品并且将流量利用到最大化。那么马赫系统是如何解决这一典型的电商问题的呢,马赫系统和流计算有什么关系呢,这是下面要详细说明的部分。三、流计算流计算是持续、低延迟、事件触发的数据处理模型。流计算模型是使用实时数据集成工具,将数据实时变化传输到流式数据存储,此时数据的传输变成实时化,将长时间累积大量的数据平摊到每个时间点不停地小批量实时传输;流计算会将计算逻辑封装为常驻计算服务,一旦启动就一直处于等待事件触发状态,当有数据流入后会触发计算迅速得到结果;当流计算得到计算结果后可以立刻将数据输出,无需等待整体数据的计算结果。闲鱼实时选品系统使用的流计算框架是Blink,Blink是阿里巴巴基于开源流计算框架Flink定制研发的企业级流计算框架,可以认为是Flink的加强版,现在已经开源。Flink是一个高吞吐、低延迟的计算引擎,同时还提供很多高级功能。比如它提供有状态的计算,支持状态管理,支持强一致性的数据语义以及支持Event Time,WaterMark对消息乱序的处理等特性,为闲鱼实时选品系统的超低延时选品提供了有力支持。3.1、Blink之StateState是指流计算过程中计算节点的中间计算结果或元数据属性,比如在aggregation过程中要在state中记录中间聚合结果,比如Apache Kafka作为数据源时候,我们也要记录已经读取记录的offset,这些State数据在计算过程中会进行持久化(插入或更新)。所以Blink中的State就是与时间相关的,Blink任务的内部数据(计算数据和元数据属性)的快照。马赫系统会在State中保存商品合并之后的全部数据和规则运行结果数据。当商品发生变更后,马赫系统会将商品变更信息与State保存的商品信息进行合并,并将合并的信息作为入参运行所有规则,最后将规则运行结果与State保存的规则运行结果进行Diff后得到最终有效的运行结果。所以Blink的State特性是马赫系统依赖的关键特性。3.2、Blink之WindowBlink的Window特性特指流计算系统特有的数据分组方式,Window的创建是数据驱动的,也就是说,窗口是在属于此窗口的第一个元素到达时创建。当窗口结束时候删除窗口及状态数据。Blink的Window主要包括两种,分别为滚动窗口(Tumble)和滑动窗口(Hop)。滚动窗口有固定大小,在每个窗口结束时进行一次数据计算,也就是说滚动窗口任务每经过一次固定周期就会进行一次数据计算,例如每分钟计算一次总量。滑动窗口与滚动窗口类似,窗口有固定的size,与滚动窗口不同的是滑动窗口可以通过slide参数控制滑动窗口的新建频率。因此当slide值小于窗口size的值的时候多个滑动窗口会重叠,此时数据会被分配给多个窗口,如下图所示:Blink的Window特性在数据计算统计方面有很多使用场景,马赫系统主要使用窗口计算系统处理数据的实时速度和延时,用来进行数据统计和监控告警。3.3、Blink之UDXUDX是Blink中用户自定义函数,可以在任务中调用以实现一些定制逻辑。Blink的UDX包括三种,分别为:UDF - User-Defined Scalar FunctionUDF是最简单的自定义函数,输入是一行数据的任意字段,输出是一个字段,可以实现数据比较、数据转换等操作。UDTF - User-Defined Table-Valued FunctionUDTF 是表值函数,每个输入(单column或多column)返回N(N>=0)Row数据,Blink框架提供了少量的UDTF,比如:STRING_SPLIT,JSON_TUPLE和GENERATE_SERIES3个built-in的UDTF。UDAF - User-Defined Aggregate FunctionUDAF是聚合函数,输入是多行数据,输出是一个字段。Blink框架Built-in的UDAF包括MAX,MIN,AVG,SUM,COUNT等,基本满足了80%常用的集合场景,但仍有一定比例的复杂业务场景,需要定制自己的聚合函数。马赫系统中使用了大量的UDX进行逻辑定制,包括消息解析、数据处理等。而马赫系统最核心的商品数据合并、规则运行和结果Diff等流程就是通过UDAF实现的。四、秒级选品方案选品系统在项目立项后也设计有多套技术方案。经过多轮讨论后,最终决定对两套方案实施验证后决定最终实现方案。第一套方案是基于PostgreSQL的方案,PostgreSQL可以很便捷的定义Function进行数据合并操作,在PostgreSQL的trigger上定义执行规则逻辑。基于PostgreSQL的技术实现较复杂,但能满足功能需求。不过性能测试结果显示PostgreSQL处理小数据量(百万级)性能较好;当trigger数量多、trigger逻辑复杂或处理亿级别数据时,PostgreSQL的性能会有较大下滑,不能满足秒级选品的性能指标。因此基于PostgreSQL的方案被否决(在闲鱼小商品池场景中仍在使用)。第二套方案是基于Blink流计算方案,通过验证发现Blink SQL很适合用来表达数据处理逻辑而且Blink性能很好,综合对比之后最终选择Blink流计算方案作为实际实施的技术方案。为了配合使用流计算方案,马赫系统经过设计和解耦,无缝对接Blink计算引擎。其中数据处理模块是马赫系统核心功能模块,负责接入商品相关各类数据、校验数据、合并数据、执行规则和处理执行结果并输出等步骤,所以数据处理模块的处理速度和延时在很大程度上能代表马赫系统数据处理速度和延时。接下来我们看下数据处理模块如何与Blink深度结合将数据处理延迟降到秒级。数据处理模块结构如上图,包含数据接入层、数据合并层、规则运行层和规则运行结果处理层。每层都针对流计算处理模式进行了单独设计。4.1、数据接入层数据接入层是数据处理模块前置,负责对接多渠道各种类型的业务数据,主要逻辑如下:数据接入层对接多个渠道多种类型的业务数据;解析业务数据并做简单校验;统计各渠道业务数据量级并进行监控,包括总量和同比变化量;通过元数据中心获取字段级别的Metadata配置。元数据中心是用来保存和管理所有字段的MetaData配置信息组件。Metadata配置代表字段元数据配置,包括字段值类型,值范围和值格式等基础信息;根据Metadata配置进行字段级别数据校验;按照马赫定义的标准数据范式组装数据。这样设计的考虑是因为业务数据是多种多样的,比如商品信息包括数据库的商品表记录、商品变更的MQ消息和算法产生的离线数据,如果直接通过Blink对接这些业务数据源的话,需要创建多个Blink任务来对接不同类型业务数据源,这种处理方式太重,而且数据接入逻辑与Blink紧耦合,不够灵活。数据接入层可以很好的解决上述问题,数据接入层可以灵活接入多种业务数据,并且将数据接入与Blink解耦,最终通过同一个Topic发出消息。而Blink任务只要监听对应的Topic就可以连续不断的收到业务数据流,触发接下来的数据处理流程。4.2、数据合并层数据合并是数据处理流程的重要步骤,数据合并的主要作用是将商品的最新信息与内存中保存的商品信息合并供后续规则运行使用。数据合并主要逻辑是:监听指定消息队列Topic,获取业务数据消息;解析消息,并将消息内容按照字段重新组装数据,格式为{key:[timestamp, value]},key是字段名称,value是字段值,timestamp为字段数据产生时间戳;将组装后的数据和内存中保存的历史数据根据timestamp进行字段级别数据合并,合并算法为比较timestamp大小取最新字段值,具体逻辑见下图。数据合并有几个前提:内存可以保存存量数据;这个是Blink提供的特性,Blink可以将任务运行过程中产生的存量数据保存在内存中,在下一次运行时从内存中取出继续处理。合并后的数据能代表商品的最新状态;这点需要一个巧妙设计:商品信息有很多字段,每个字段的值是数组,不仅要记录实际值,还要记录当前值的修改时间戳。在合并商品信息时,按照字段进行合并,合并规则是取时间戳最大的值为准。举例来说,内存中保存的商品ID=1的信息是{“desc”: [1, “描述1”], “price”: [4, 100.5]},数据流中商品ID=1的信息是{“desc”: [2, “描述2”], “price”: [3, 99.5]},那么合并结果就是{“desc”: [2, “描述2”], “price”: [4, 100.5]},每个字段的值都是最新的,代表商品当前最新信息。当商品信息发生变化后,最新数据由数据接入层流入,通过数据合并层将数据合并到内存,Blink内存中保存的是商品当前最新的全部数据。4.3、规则运行层规则运行层是数据处理流程核心模块,通过规则运算得出商品对各规则命中结果,逻辑如下:规则运行层接受输入为经过数据合并后的数据;通过元数据中心获取字段级别Metadata配置;根据字段Metadata配置解析数据;通过规则中心获取有效规则列表,规则中心是指创建和管理规则生命周期的组件;循环规则列表,运行单项规则,将规则命中结果保存在内存;记录运行规则抛出异常的数据,并进行监控告警。这里的规则指的是运营创建的业务规则,比如商品价格大于50且状态为在线。规则的输入是经过数据合并后的商品数据,输出是true或false,即是否命中规则条件。规则代表的是业务投放场景,马赫系统的业务价值就是在商品发生变更后尽快判断是否命中之前未命中的规则或是不命中之前已经命中的规则,并将命中和不命中结果尽快体现到投放场景中。规则运行需利用Blink强大算力来保证快速执行,马赫系统当前有将近300条规则,而且还在快速增长。这意味着每个商品发生变更后要在Blink上运行成百上千条规则,闲鱼每天有上亿商品发生变更,这背后需要的运算量是非常惊人的。4.4、运行结果处理层读者读到这里可能会奇怪,明明经过规则运行之后直接把运行结果输出到投放场景就可以了,不需要运行结果处理层。实际上运行结果处理层是数据处理模块最重要的部分。因为在实际场景中,商品的变更在大部分情况只会命中很少一部分规则,而且命中结果也很少会变化。也就是说商品对很多规则的命中结果是没有意义的,如果将这些命中结果也输出的话,只会增加操作TPS,对实际结果没有任何帮助。而筛选出有效的运行结果,这就是运行结果处理层的作用。运行结果处理层逻辑如下:获取商品数据的规则运行结果;按照是否命中规则解析运行结果;将运行结果与内存中保存的历史运行结果进行diff,diff作用是排除新老结果中相同的命中子项,逻辑见下图。运行结果处理层利用Blink内存保存商品上一次变更后规则运行结果,并将当前变更后规则运行结果与内存中结果进行比较,计算出有效运行结果。举例来说,商品A上一次变更后规则命中结果为{“rule1”:true, “rule2”:true, “rule3”:false, “rule4”:false},当前变更后规则命中结果为{“rule1”:true, “rule2”:false, “rule3”:false, “rule4”:true}。因为商品A变更后对rule1和rule3的命中结果没有变化,所以实际有效的命中结果是{“rule2”:false, “rule4”:true},通过运行结果处理层处理后输出的是有效结果的最小集,可以极大减小无效结果输出,提高数据处理的整体性能和效率。4.5、难点解析虽然闲鱼实时选品系统在立项之初经过预研和论证,但因为使用很多新技术框架和流计算思路,在开发过程中遇到一些难题,包括设计和功能实现方面的,很多是设计流计算系统的典型问题。我们就其中一个问题与各位读者探讨-规则公式转换。4.5.1、规则公式转换这个问题的业务场景是:运营同学在马赫系统页面上筛选商品字段后保存规则,服务端是已有的老系统,逻辑是根据规则生成一段SQL,SQL的where条件和运营筛选条件相同。SQL有两方面的作用,一方面是作为离线规则,在离线数据库中执行SQL筛选符合规则的离线商品数据;另一方面是转换成在线规则,在Blink任务中对实时商品变更数据执行规则以判断是否命中。因为实时规则运行使用的是MVEL表达式引擎,MVEL表达式是类Java语法的,所以问题就是将离线规则的SQL转换成在线规则的Java表达式,两者逻辑需一致,并且需兼顾性能和效率。问题的解决方案很明确,解析SQL后将SQL操作符转换成Java操作符,并将SQL特有语法转成Java语法,例如A like ‘%test%‘转成A.contains(’test’)。这个问题的难点是如何解析SQL和将解析后的语义转成Java语句。经过调研之后给出了简单而优雅的解决方案,主要步骤如下:使用Druid框架解析SQL语句,转成一个二叉树,单独取出其中的where条件子树;通过后序遍历算法遍历where条件子树;将SQL操作符换成对应的Java操作符;目前支持且、或、等于、不等于、大于、大于等于、小于、小于等于、like、not like和in等操作。将SQL语法格式转成Java语法;将in语法改成Java的或语法,例如A in (‘hello’, ‘world’)转成(A == ‘hello’) || (A == ‘world’)。实际运行结果如下:代码逻辑如下(主要是二叉树后续遍历和操作符转换,不再详细解释):五、结论马赫系统上线以来,已经支持近400场活动和投放场景,每天处理近1.4亿条消息,峰值TPS达到50000。马赫系统已经成为闲鱼选品投放的重要支撑。本文主要阐述马赫系统中数据处理的具体设计方案,说明整体设计的来龙去脉。虽然闲鱼实时选品系统针对的是商品选品,但数据处理流计算技术方案的输入是MQ消息,输出也是MQ消息,不与具体业务绑定,所以数据处理流计算技术方案不只适用于商品选品,也适合其他类似实时筛选业务场景。希望我们的技术方案和设计思路能给你带来一些想法和思考,也欢迎和我们留言讨论,谢谢。参考资料闲鱼实时选品系统:https://mp.weixin.qq.com/s/8ROsZniYD7nIQssC14mn3wBlink:https://github.com/apache/flink/tree/blinkPostgreSQL:https://www.postgresql.org/druid:https://github.com/alibaba/druid本文作者:闲鱼技术-剑辛阅读原文本文为云栖社区原创内容,未经允许不得转载。

April 3, 2019 · 1 min · jiezi

Node.js 应用故障排查手册 —— 利用 CPU 分析调优吞吐量

楔子在我们想要新上线一个 Node.js 应用之前,尤其是技术栈切换的第一个 Node.js 应用,由于担心其在线上的吞吐量表现,肯定会想要进行性能压测,以便对其在当前的集群规模下能抗住多少流量有一个预估。本案例实际上正是在这样的一个场景下,我们想要上线 Node.js 技术栈来做前后端分离,那么刨开后端服务的响应 QPS,纯使用 Node.js 进行的模板渲染能有怎么样的表现,这是大家非常关心的问题。本书首发在 Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区会同步更新。优化过程集群在性能压测下反映出来的整体能力其实由单机吞吐量就可以测算得知,因此这次的性能压测采用的 4 核 8G 内存的服务器进行压测,并且页面使用比较流行的 ejs 进行服务端渲染,进程数则按照核数使用 PM2 启动了四个业务子进程来运行。1. 开始压测完成这些准备后,使用阿里云提供的 PTS 性能压测工具进行压力测试,此时大致单机 ejs 模板渲染的 QPS 在 200 左右,此时通过 Node.js 性能平台 监控可以看到四个子进程的 CPU 基本都在 100%,即 CPU 负载达到了瓶颈,但是区区 200 左右的 QPS 显然系统整体渲染非常的不理想。2. 模板缓存因为是 CPU 达到了系统瓶颈导致整体 QPS 上不去,因此按照第二部分工具篇章节的方法,我们在平台上抓了 压测期间 的 3 分钟的 CPU Profile,展现的结果如下图所示:这里就看到了很奇怪的地方,因为压测环境下我们已经打开了模板缓存,按理来说不会再出现 ejs.compile 函数对应的模板编译才对。仔细比对项目中的渲染逻辑代码,发现这部分采用了一个比较不常见的模块 koa-view,项目开发者想当然地用 ejs 模块地入参方式传入了 cache: true,但是实际上该模块并没有对 ejs 模板做更好的支持,因此实际压测情况下模板缓存并没有生效,而模板地编译动作本质上字符串处理,它恰恰是一个 CPU 密集地操作,这就导致了 QPS 达不到预期的状况。了解到原因之后,首先我们将 koa-view 替换为更好用的 koa-ejs 模块,并且按照 koa-ejs 的文档正确开启缓存:render(app, { root: path.join(__dirname, ‘view’), viewExt: ‘html’, cache: true});再次进行压测后,单机下的 QPS 提升到了 600 左右,虽然大约提升了三倍的性能,但是仍然达不到预期的目标。3. include 编译为了继续优化进一步提升服务器的渲染性能,我们继续在压测期间抓取 3 分钟的 CPU Profile 进行查看:可以看到,我们虽然已经确认使用 koa-ejs 模块且正确开启了缓存,但是压测期间的 CPU Profile 里面竟然还有 ejs 的 compile 动作!继续展开这里的 compile,发现是 includeFile 时引入的,继续回到项目本身,观察压测的页面模板,确实使用了 ejs 注入的 include 方法来引入其它模板:<%- include("../xxx") %>对比 ejs 的源代码后,这个注入的 include 函数调用链确实也是 include -> includeFile -> handleCache -> compile,与压测得到的 CPU Profile 展示的内容一致。那么下面红框内的 replace 部分也是在 compile 过程中产生的。到了这里开始怀疑 koa-ejs 模块没有正确地将 cache 参数传递给真正负责渲染地 ejs 模块,导致这个问题地发生,所以继续去阅读 koa-ejs 的缓存设置,以下是简化后的逻辑(koa-ejs@4.1.1 版本):const cache = Object.create(null);async function render(view, options) { view += settings.viewExt; const viewPath = path.join(settings.root, view); // 如果有缓存直接使用缓存后的模板解析得到的函数进行渲染 if (settings.cache && cache[viewPath]) { return cache[viewPath].call(options.scope, options); } // 没有缓存首次渲染调用 ejs.compile 进行编译 const tpl = await fs.readFile(viewPath, ‘utf8’); const fn = ejs.compile(tpl, { filename: viewPath, _with: settings._with, compileDebug: settings.debug && settings.compileDebug, debug: settings.debug, delimiter: settings.delimiter }); // 将 ejs.compile 得到的模板解析函数缓存起来 if (settings.cache) { cache[viewPath] = fn; } return fn.call(options.scope, options);}显然,koa-ejs 模板的模板缓存是完全自己实现的,并没有在调用 ejs.compile 方法时传入的 option 参数内将用户设置的 cache 参数传递过去而使用 ejs 模块提供的 cache 能力。但是偏偏项目在模板内又直接使用了 ejs 模块注入的 include 方法进行模板间的调用,产生的结果就是只缓存了主模板,而主模板使用 include 调用别的模板还是会重新进行编译解析,进而造成压测下还是存在大量重复的模板编译动作导致 QPS 升不上去。再次找到了问题的根源,为了验证是否是 koa-ejs 模块本身的 bug,我们在项目中将其渲染逻辑稍作更改:const fn = ejs.compile(tpl, { filename: viewPath, _with: settings._with, compileDebug: settings.debug && settings.compileDebug, debug: settings.debug, delimiter: settings.delimiter, // 将用户设置的 cache 参数传递给 ejs 而使用到其提供的缓存能力 cache: settings.cache});然后打包后进行压测,此时单机 QPS 从 600 提升至 4000 左右,基本达到了上线前的性能预期,为了确认压测下是否还有模板的编译动作,我们继续在 Node.js 性能平台 上抓取压测期间 3 分钟的 CPU Profile:可以看到上述对 koa-ejs 模板进行优化后,ejs.compile 确实消失了,而压测期间不再有大量重复且耗费 CPU 的编译动作后,应用整体的性能比最开始有了 20 倍左右的提升。文中 koa-ejs 模块缓存问题已经在 4.1.2 版本(包含)之后被修复了,详情可以见 cache include file,如果大家使用的 koa-ejs 版本 >= 4.1.2 就可以放心使用。结尾CPU Profile 本质上以可读的方式反映给开发者运行时的 JavaScript 代码执行频繁程度,除了在线上进程出现负载很高时能够用来定位问题代码之外,它在我们上线前进行性能压测和对应的性能调优时也能提供巨大的帮助。这里需要注意的是:仅当进程 CPU 负载非常高的时候去抓取得到的 CPU Profile 才能真正反馈给我们问题所在。在这个源自真实生产的案例中,我们也可以看到,正确和不正确地去使用 Node.js 开发应用其前后运行效率能达到二十倍的差距,Node.js 作为一门服务端技术栈发展至今日,其本身能够提供的性能是毋庸置疑的,绝大部分情况下执行效率不佳是由我们自身的业务代码或者三方库本身的 Bug 引起的,Node.js 性能平台 则可以帮助我们以比较方便的方式找出这些 Bug。本文作者:奕钧阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 2, 2019 · 2 min · jiezi

藏经阁计划发布一年,阿里知识引擎有哪些技术突破?

阿里妹导读:2018年4月阿里巴巴业务平台事业部——知识图谱团队联合清华大学、浙江大学、中科院自动化所、中科院软件所、苏州大学等五家机构,联合发布藏经阁(知识引擎)研究计划。藏经阁计划依赖阿里强大的计算能力(例如Igraph图数据库),和先进的机器学习算法(例如PAI平台)。计划发布一年以来,阿里知识图谱团队有哪些技术突破?今天一起来了解。背景藏经阁计划发布一年以来,我们对知识引擎技术进行了重新定义,将其定义成五大技术模块:知识获取、知识建模、知识推理、知识融合、知识服务,并将其开发落地。其中知识建模的任务是定义通用/特定领域知识描述的概念、事件、规则及其相互关系的知识表示方法,建立通用/特定领域知识图谱的概念模型;知识获取是对知识建模定义的知识要素进行实例化的获取过程,将非结构化数据结构化为图谱里的知识;而知识融合是对异构和碎片化知识进行语义集成的过程,通过发现碎片化以及异构知识之间的关联,获得更完整的知识描述和知识之间的关联关系,实现知识互补和融合;知识推理是根据知识图谱提供知识计算和推理模型,发现知识图谱中的相关知识和隐含知识的过程。知识服务则是通过构建好的知识图谱提供以知识为核心的知识智能服务,提升应用系统的智能化服务能力。经过一年的工作,在知识建模模块我们开发了Ontology自动搭建、属性自动发现等算法,搭建了知识图谱Ontology构建的工具;在知识获取模块我们研发了新实体识别、紧凑型事件识别,关系抽取等算法,达到了业界最高水平;在知识融合模块,我们设计了实体对齐和属性对齐的深度学习算法,使之可以在不同知识库上达到更好的扩展性,大大丰富了知识图谱里的知识;在知识推理模块,我们提出了基于Character Embedding的知识图谱表示学习模型CharTransE、可解释的知识图谱学习表示模型XTransE,并开发出了强大的推理引擎。基于上面的这些技术模块,我们开发了通用的知识引擎产品,目前已经在全阿里经济体的淘宝、天猫、盒马鲜生、飞猪、天猫精灵等几十种产品上取得了成功应用,每天有8000多万次在线调用,日均离线输出9亿条知识。目前在知识引擎产品上,已经构建成功并运行着商品、旅游、新制造等5个垂直领域图谱的服务。在每个模块的构建过程中,我们陆续攻克了一系列的技术问题。本文将选取其中的两项工作来介绍给大家:1、在众包数据上进行对抗学习的命名实体识别方法知识获取模块包含实体识别、实体链接、新实体发现、关系抽取、事件挖掘等基本任务,而实体识别(NER)又是其中最核心的任务。目前学术界最好的命名实体识别算法主要是基于有监督学习的。构建高性能NER系统的关键是获取高质量标注语料。但是高质量标注数据通常需要专家进行标注,代价高并且速度较慢,因此目前工业界比较流行的方案是依赖众包来标注数据,但是由于众包人员素质参差不齐,对问题理解也千差万别,所以用其训练的算法效果会受到影响。基于此问题,我们提出了针对众包标注数据,设计对抗网络来学习众包标注员之间的共性,消除噪音,提高中文NER的性能的方法。这项工作的具体网络框架如图3所示:标注员ID:对于各个标注员ID信息,我们使用一个Looking-up表,表内存储着每个WorkerID的向量表示。向量的初始值通过随机数进行初始化。在模型训练过程中,ID向量的所有数值作为模型的参数,在迭代过程中随同其他参数一起优化。在训练时每个标注样例的标注员,我们直接通过查表获取对应的ID向量表示。在测试时,由于缺乏标注员信息,我们使用所有向量的平均值作为ID向量输入。对抗学习(WorkerAdversarial):众包数据作为训练语料,存在一定数量的标注错误,即“噪音”。这些标注不当或标注错误都是由标注员带来的。不同标注员对于规范的理解和背景认识是不同的。对抗学习的各LSTM模块如下:私有信息的LSTM称为“private”,它的学习目标是拟合各位标注员的独立分布;而共有信息的LSTM称为“common”,它的输入是句子,它的作用是学习标注结果之间的共有特征,。标注信息的LSTM称为“label”,以训练样例的标注结果序列为输入,再通过标注员分类器把label和common的LSTM特征合并,输入给CNN层进行特征组合提取,最终对标注员进行分类。要注意的是,我们希望标注员分类器最终失去判断能力,也就是学习到特征对标注员没有区分能力,也就是共性特征。所以在训练参数优化时,它要反向更新。在实际的实体识别任务中,我们把common和private的LSTM特征和标注员ID向量合并,作为实体标注部分的输入,最后用CRF层解码完成标注任务。实验结果如图4所示,我们的算法在商品Title和用户搜索Query的两个数据集上均取得最好的性能:2、基于规则与graph embedding迭代学习的知识图谱推理算法知识图谱推理计算是补充和校验图谱关系及属性的必不可少的技术手段。规则和嵌入(Embedding)是两种不同的知识图谱推理的方式,并各有优劣,规则本身精确且人可理解,但大部分规则学习方法在大规模知识图谱上面临效率问题,而嵌入(Embedding)表示本身具有很强的特征捕捉能力,也能够应用到大规模复杂的知识图谱上,但好的嵌入表示依赖于训练信息的丰富程度,所以对稀疏的实体很难学到很好的嵌入表示。我们提出了一种迭代学习规则和嵌入的思路,在这项工作中我们利用表示学习来学习规则,并利用规则对稀疏的实体进行潜在三元组的预测,并将预测的三元组添加到嵌入表示的学习过程中,然后不断进行迭代学习。工作的整体框架如图5所示:嵌入学习优化的目标函数是:其中:lsro表示三元组的标记,表示三元组的评分函数,vs表示图谱三元组中主语(subject)的映射,Mr表示图谱中两个实体间关系的映射,vo表示图谱三元组中宾语(object)的映射。基于学习到的规则(axiom),就可以进行推理执行了。通过一种迭代策略,先使用嵌入(Embedding)的方法从图谱中学习到规则,再将规则推理执行,将新增的关系再加入到图谱中,通过这种不断学习迭代的算法,能够将图谱中的关系预测做的越来越准。最终我们的算法取得了非常优秀的性能:除了上述两项工作以外,在知识引擎技术的研发上我们还有一系列的前沿工作,取得了领先业界的效果,研究成果发表在AAAI、WWW、EMNLP、WSDM等会议上。之后阿里巴巴知识图谱团队会持续推进藏经阁计划,构建通用可迁移的知识图谱算法,并将知识图谱里的数据输出到阿里巴巴内外部的各项应用之中,为这些应用插上AI的翅膀,成为阿里巴巴经济体乃至全社会的基础设施。本文作者:阿里知识图谱团队 阅读原文本文来自云栖社区合作伙伴“ 阿里技术”,如需转载请联系原作者。

April 1, 2019 · 1 min · jiezi

“练好内功坚持被集成”,阿里云发布SaaS加速器

摘要: 这是发生在3月21日阿里云峰会·北京上的一幕。阿里云智能产品管理部总经理马劲在现场演示了他用5天开发完成的智能购车应用,并体验了其中的虚拟试驾功能。过去需要几十人的团队耗费一个月才能完成的应用搭建,现在通过SaaS加速器,只需要一个人、五天就能完成!在3月21日的2019阿里云峰会·北京上,阿里云发布新产品SaaS加速器:人工智能、虚拟现实等技术能力被集成为模块,ISV和开发者只要简单拖拽,就可以快速搭建SaaS应用。发布现场,阿里云智能产品管理部总经理马劲进行简单演示。通过SaaS加速器仅用五天就开发完成了一款智能购车应用,并具备虚拟试驾等功能。过去,搭建这样一个智能购车应用,可能需要几十人的团队耗费一个月才能完成。马劲表示,阿里云自己不做SaaS,要让大家来做更好的SaaS;要练好“内功”,更广泛、更深度地“被集成”。实现SaaS业务规模化的关键在于拥有一个稳定强大的平台提供支撑、出色的前台提高开发定制效率,以及可沉淀和复用的中台能力。此次阿里云推出的SaaS加速器,涵盖商业中心、能力中心、技术中心三大板块,是阿里巴巴商业、能力和技术的一次合力输出:技术能力在这里沉淀为一个个模块,ISV和开发者只要通过简单的操作,写很少的代码、甚至不写代码,就可以快速搭建一个SaaS应用,实现规模化、快速复制。同时,借助阿里巴巴的丰富生态,ISV和开发者能够快速完成应用发布,向目标用户提供服务,形成从产品研发到部署交付的完整商业闭环。本文作者:茜莹阅读原文本文为云栖社区原创内容,未经允许不得转载。

March 29, 2019 · 1 min · jiezi

阿里敏捷教练:多团队开发一个产品的组织设计和思考

摘要: Scrum等敏捷开发框架,最初都是为5到9人的小团队设计的。通过保持专注和合理利用新技术,在相当长的时间里小团队仍然可以支撑业务发展。 随着业务成长,小团队的产出可能跟不上业务需要,团队就会面临规模化的问题。Scrum等敏捷开发框架,最初都是为5到9人的小团队设计的。通过保持专注和合理利用新技术,在相当长的时间里小团队仍然可以支撑业务发展。随着业务成长,小团队的产出可能跟不上业务需要,团队就会面临规模化的问题。从1个团队拓展到3个团队,仍然可以通过简单的团队间沟通保持高效协作。当产品复杂到需要5个以上团队同时开发时,我们需要一定的组织设计来保证团队间的顺畅协作,使得多团队共同开发一个产品时仍能保持敏捷性。保持小团队在初创企业或产品刚起步时,团队通常都不大。随着业务的发展,需求越来越多,产品越来越复杂,很多团队的第一反应都是加人。事实上,加人并不是唯一选择,也未必是最优选择。很多时候,小团队能交付惊人的业务成果。一方面,通过保持专注:Do one thing and do it well,小团队可以聚焦于核心业务,摒除不必要的干扰。有一款微处理器ARM比英特尔先做出来,团队的一个leader说:“回过头来看,当时我们决定做一款微处理器的时候,我认为我做了两个重要的决定。我信任我的团队,并且给了团队两件英特尔和摩托罗拉永远不会提供给他们员工的东西:第一是缺钱,第二是缺人。他们不得不保持简单”。类似的,创办于2009年的WhatsApp于2014年被Facebook收购时,公司只有55名员工,全球活跃用户达到4.5亿人,日发送短消息达160亿条。另一方面,随着开源运动、中台技术和云化技术的发展,很多非核心业务逻辑可以借助外力快速搭建,在业务高速发展的同时,继续保持一支精干的团队。例如,在阿里巴巴研发协同平台“云效”上,二十分钟就可以搭建一套Spring Boot web application的持续集成流水线,包含静态代码扫描、单元测试、编译、打包、部署、接口测试。不仅操作方便快捷,还省去了采购机器、部署和管理 build farm的开销。业务单元特性团队即便努力保持专注并用尽了技术红利,有时业务的发展还是远远超出预期,此时组建多个团队势在必行。比较理想的选择是按照业务单元来组建特性团队。一个业务单元类似于一家小型创业公司,有自己的长期使命和愿景,有相对清晰的业务边界和盈利模式。人员方面,各业务单元有独立的业务、产品和研发团队。技术方面,各业务单元可以独立完成产品开发的全流程,包括业务决策、产品设计、开发、测试和发布,尽量避免业务单元之间的依赖。作为一个超级app,手机淘宝分为几条业务线,同一条业务线内还分为几个独立业务。例如,微淘和淘宝直播都属于内容平台业务线,二者的内容生产、传播渠道、受众和盈利模式不同,因而是相对独立的业务单元。二者有独立的业务、产品和研发团队,业务目标也分开设定和衡量。技术上解耦是各业务单元能够独立发展的前提。为了解决团队间的依赖,手机淘宝对架构做了容器化改造:一些必要的初始化操作放在common容器中,各业务在自己的bundle中。各业务bundle按需加载,只能依赖底层的common架构,不能相互依赖。这样各业务bundle可以并行开发,互不干扰。按照独立的业务边界来组建特性团队,团队能独立发布新功能,迅速获得市场反馈,通过不断试错找到业务发展的方向。全球第一大音乐平台、音乐流媒体公司Spotify也按照业务单元组建团队。在" Scaling Agile @ Spotify with Tribes, Squads, Chapters & Guilds “[1] ,敏捷教练Henrik Kniberg详细介绍了Spotify模式。Spotify的30多个“小分队”(squad)分布在全球的三个城市,每个squad负责产品的特定方向(例如搜索或radio)。每个squad相当于一个小创业公司,squad没有特定的主管,只有一位产品负责人(Product Owner)。PO负责业务方向,squad成员组成跨职能团队交付业务结果。PO帮助squad制定目标和管理优先级,也会定期维护公司层面的产品路线图并确保squad的目标与公司战略相匹配。squad被鼓励应用精益创业原则,例如先交付MVP(minimum viable product),并通过A/B测试来验证假设。此外,squad可以得到敏捷教练的帮助,敏捷教练引导squad持续改进并帮助团队移除障碍。在squad之上,spotify还有两层组织架构:具有相关专业知识的人横向组成“分会”(chapter),工作在相似领域的squad组成“部落”(tribe)。此外,具有相同兴趣的人组成“行会”(guild)。这套架构的主要目的,是促进全公司范围的信息和知识共享。员工向chapter lead汇报,在转换squad时汇报线不变。尽管看上去像普通的矩阵式组织,这个矩阵是向产品交付倾斜的。同一个squad的成员坐在一起,组成高度自治的跨职能敏捷团队,共同决定产品目标以及如何交付产品。横向的chapter维度只是为了更方便地共享知识、工具和代码。chapter lead的工作是引导和支持信息流动和知识共享,而不会像传统职能经理那样负责分配工作。注:图片来自于https://blog.crisp.se/2012/11/14/henrikkniberg/scaling-agile-at-spotify与此类似,淘宝直播的业务、产品和研发团队也汇报给不同的职能经理。高度统一的业务目标把团队成员凝聚在一起,团队共同决定业务方向、业务目标以及如何达成目标。职能经理为业务发展提供支持和帮助,并帮助团队成员在职业道路上成长,并不会把主要精力放在具体的产品交付上。淘宝直播敏捷实践参见《阿里敏捷教练,全面解析淘宝直播敏捷实践之路》。无限制特性团队有时团队在业务发展时壮大了,但是经过了一段高速发展,原有的业务方向遇到了瓶颈,新的业务方向还在摸索中。此时,业务方向还不明朗,难以按照明确的业务单元组建团队,团队需要快速适应业务方向的变化。此时,要鼓励团队广度学习,避免局部优化。不同于围绕业务单元组建的特性团队,无限制特性团队没有相对独立的业务领域,多个特性团队共享一份产品代办列表(Product Backlog),按照统一的优先级交付产品功能。无限制特性团队,并非所有团队都相同的无差别特性团队,每个团队还是可以有自己的特色和专长,只要多个团队组合起来能够按照Product Backlog的优先级交付特性即可。2018年3月,我支持阿里健康互联网医疗业务线时,正遇到这样的情况:互联网医疗业务经过两年多的摸索,找到了一些可能的发展方向,但是还没有找到非常明确的盈利模式,多个方向都需要进一步尝试。研发团队包括服务端开发、H5开发、Android开发、iOS开发、测试等30多位同学。在原有的资源池模式下,每月职能经理按照产品经理的输入,分配研发同学到各个项目中。由于业务的复杂性,产品涉及的核心应用有15个以上,除了电商平台的商品、库存、营销等基本功能,还包含互联网医疗特有的问诊、挂号等服务,并涉及到算法和AI。人员技能的瓶颈非常突出:部分核心应用只有一位同学特别了解。2018年4月至5月,商品模块负责人和AI问诊模块负责人先后休假,相应模块的技术方案设计几乎停滞,严重拖累进度。为了平衡复杂的人员技能和项目需要,职能经理经常绞尽脑汁,仍然不免捉襟见肘,一线同学身兼多个项目非常普遍。多个项目都依赖同一位团队成员时,不得不串行等待。在多个项目间频繁切换也增加了上下文切换成本。为了解决人员技能瓶颈的痛点,同时考虑到互联网医疗特定的业务发展阶段,尝试了无限制特性团队共同交付一个产品的协作模式:30人自由组合成两支特性团队。组队只需满足约束条件:人数均衡,核心应用在每个团队都有人了解,新老结合,男女搭配。组队成功后,两支团队从同一份Product Backlog里按照优先级领需求。如果某个团队无法独立完成当前最高优先级的需求,先由这个团队认领,另一个团队派师傅指导。师傅主要是培养徒弟,具体工作由认领团队的同学动手完成。由于资源瓶颈的限制,2018年5月1日到6月14日需求交付的累计偏差(需求实际交付日期与计划交付日期的偏差累加)达到了151天。经过两个月的努力,两支特性团队都具备了完成各类需求的能力,团队可以完全按照Product Backlog的优先级领需求,既不需要团队成员并发支持多个项目,也不需要等待资源瓶颈的释放。6月15日到7月31日的累计交付偏差缩短到了3天。8月1日到8月31日继续保持准时交付,累计交付偏差为2天。团队成员的个人能力得到了充分锻炼,主动拓展技能承担重任的同学获得了晋升,得到了认可。团队的自组织能力也得到了发展,遇到问题和阻碍,团队成员会主动想办法解决,不再事事依赖职能经理。职能经理的角色从派活变成了辅导和帮助团队,减少了救火时间,有更多时间考虑团队的长远发展。综上,无限制特性团队方案解决了业务需求等待资源瓶颈的痛点,不是让业务发展来匹配人员的技能,而是人员拓展技能匹配业务发展的需要。与此同时,团队成员的个人能力得到了锻炼,团队的自组织能力得到了发展,也解放了职能经理。无论是业务单元特性团队,还是无限制特性团队,每个团队都要具有独立交付产品特性的能力。一个复杂的产品特性,通常都需要修改多个模块才能实现。多个团队修改同一个模块时,如何保证模块设计的一致性,并及时清理代码偿还技术债?引入模块守护者通常是个有益的实践:每个模块最好有两位模块守护者互相backup,修改模块代码需要请模块守护者做code review,一些复杂的修改最好预先进行设计评审。模块守护者可以是兼职的,只要保证每周抽出一定比例的时间维护模块代码即可。随着业务方向越来越清晰,业务模式逐渐稳定,无限制特性团队会逐步找到相对固定的分工合作模式,每个特性团队会逐步找到自己最擅长和最感兴趣的产品方向。明确的产品方向,为团队提供了长期深耕的条件,团队逐步成为某一领域的专家。此时,无限制特性团队就完成了向业务单元特性团队的过渡。小结通过手机淘宝、Spotify和阿里健康的案例,我相信多团队开发一个产品仍然可以保持敏捷。在业务方向明确的情况下,按照业务单元组建特性团队是最理想的选择。在业务方向不明朗的情况下,可以先组建无限制特性团队,再逐步过渡到业务单元特性团队。无论采用何种组织设计,目的都是快速跑通业务闭环:持续地交付业务价值,并在真正的市场环境中检验假设,通过快速试错找到在一定的利润水平上为企业或终端用户提供产品和服务的可行方法。参考文献:[1] https://blog.crisp.se/2012/11/14/henrikkniberg/scaling-agile-at-spotify作者:张迎辉,花名问菊,阿里巴巴敏捷教练,罗汉堂讲师,开发和讲授多门敏捷课程。先后支持手机淘宝、优酷、阿里文娱广告、阿里健康等多个部门的团队敏捷转型。亲身感受到敏捷给团队带来的改变,立志成为敏捷践行者.阅读作者更多内容:阿里敏捷教练,全面解析淘宝直播敏捷实践之路敏捷团队的病与药——阿里健康B2B团队敏捷转型手记打造真正的One Team,持续快速交付价值——阿里文娱广告团队敏捷实践阿里敏捷教练如何优化优酷需求分析流程?本文作者:云效鼓励师阅读原文本文为云栖社区原创内容,未经允许不得转载。

March 26, 2019 · 1 min · jiezi

feflow 插件实现原理

最近在着手接入和推进研发的规范化、流程化,使得团队开发风格更加统一,提升研发质量与效率。在接入的过程中,选择 feflow 与 现有的相关流程相结合管理脚手架升级和项目初始化,后续考虑开发或者使用插件处理更多业务开发流程。了解插件实现原理,有利于后续插件开发和使用;学习其设计方式,对其他项目开发大有裨益。feflow 是前端研发规范化和流程化工具,常见的内置指令有: init、 dev、build、deploy等,从初始化到构建都提供内置指令,如若需要额外指令实现特定功能,就需要自定定义插件;本质上feflow是一个插件加载工具,可以让开发者开发适用于业务的插件。本文主要介绍 feflow 是如何进行插件注册、加载和使用。一、插件注册feflow 作为一个命令行执行工具,自然而然地支持命令注册,无论是内置指令还是自定义插件,都需要注册相关命令的。首先看一下如何注册指令,以 feflow-plugin-example 为例,其代码如下:feflow.cmd.register(‘add’, ‘加法运算器’, function(args) { // do something add(args._);});注册指令需要的参数如下:name 必填,指令名称desc 必填,阐述相关指令的作用options 可选,函数相关可选项fn 必填,指令调用的回调函数指令注册是 feflow 中 Commond 对象来实现的,Commond 中定义 store 对象,其会存储相关指令,并把指令和指令相关功能做一一映射。关键代码如下: funciton register(name, desc, options, fn) { //… format parameter const c = this.store[name.toLowerCase()] = fn; c.options = options; c.desc = desc; //provide an alias for the instruction. //feflow version is equal to feflow v(ersion) this.alias = abbrev(Object.keys(this.store)); }二、插件加载插件注册安装之后,都会存储在 feflow 安装的 node_modules目录下,在 feflow 进行初始化的时候,首先会读取以feflow-plugin-开头的文件夹,然后进行插件动态加载。2.1 模块初始化动态加载是模拟 Node 模块加载方式的实现,首先会初始化 module 模块。其中会包含模块相关路径、文件名称等相关信息。在进行模块加载时会进行路径查找,即 module.paths。const module = new Module(path);module.filename = path;module.paths = Module._nodeModulePaths(path);2.2 编译调用编译执行时首先会通过fs.readFile去获取对应的文件内容,接下来重点看一下是如何编译调用的,代码片段如下:script = ‘(function(exports, require, module, __filename, __dirname, feflow){’ +script + ‘});’; //(1)const fn = vm.runInThisContext(script, path); //(2)return fn(module.exports, require, module, path, pathFn.dirname(path), self); //(3)首先看到是对原始内容进行封装(标注1),会传入CommonJS 相关规范实现的 exports, require, module, _filename, _dirname 以及注入的自定义变量 feflow。经过封装的内容如下:(function (exports, require, module, __filename, __dirname) { //原始内容});经过包装后返回的字符串,会作为 vm.runInThisContext() 方法的输入参数(标注2)。vm 模块简单的来说就是用来做沙箱环境执行代码,对代码的上下文环境做隔离。vm.runInThisContext 类似 eval,不同的是生成的代码运行时可以访问外部的 global 对象,但是不能访问其他变量。接下来会传入当前 feflow 实例,调用自定义指令的注册(标注3)。三、插件使用在插件使用前确保已经全局安装 feflow-cli:npm install -g feflow-cli对于插件使用分两种情况:(1) 插件开发之后发布到 NPM 上或者 NPM 私有仓库(2) 插件未发布下面分别详细介绍使用步骤。插件已发布1.插件进行安装:feflow install feflow-plugin-example2.调用对应指令:feflow add 1 2插件未发布1.在插件目录下 npm link:cd feflow-plugin-examplenpm link2.到 feflow 安装目录 .feflow 中安装npm link feflow-plugin-example3.编辑 .feflow/package.json 文件,加入依赖dependencies: { //… “feflow-plugin-example”: “1.0.0”}修改完成之后就可以进行调用。 ...

March 25, 2019 · 1 min · jiezi

阿里巴巴复杂搜索系统的可靠性优化之路

背景搜索引擎是电商平台成交链路的核心环节,搜索引擎的高可用直接影响成交效率。闲鱼搜索引擎作为闲鱼关键系统,复杂度和系统体量都非常高,再加上闲鱼所有导购场景都依靠搜索赋能,搜索服务的稳定可靠成为了闲鱼大部分业务场景可用能力的衡量标准;如何保障搜索服务的稳定和高可用成为了极大的挑战。闲鱼搜索作为闲鱼核心系统,有以下几个突出的特点:数据体量大:对接闲鱼数十亿的商品,引擎有效商品数亿;索引庞大:闲鱼非结构化商品需要与算法团队合作,预测抽取有价值的结构化信息,建立索引;已创建数百的索引字段,整个引擎索引数据量为T级别;增量消息多:日常增量消息QPS 数十万,峰值QPS可以达到 数百万;查询复杂:很多特殊业务场景,查询条件要求苛刻而复杂;比如召回GROUP分组统计,聚合/打散/去重,关键词复合运算查询等;实时性性要求高:闲鱼中都是二手商品,卖家商品的库存都是1;商品上下架频繁,对引擎数据的同步更新实时性要求非常高;智能化扩展:由于闲鱼商品非结构化特性,为保障召回数据的效果以及相关性;需要引擎具备智能插件扩展的能力,能与算法开发人员协同;鉴于闲鱼商品搜索引擎以上主要特点,本文详细介绍闲鱼搜索在系统高可用上做的各种努力,希望能给读者一些启发。闲鱼搜索整体架构正式引出搜索稳定性保障方案之前,我们需要对闲鱼搜索技术有一个简单大致的了解;我们比较过很多外部开源的搜索引擎,都不能完美支持背景中所列的需求点;闲鱼使用的是阿里巴巴最新研发的搜索引擎平台Ha3,Ha3是一款非常高效,智能强大的搜索引擎,它完全满足闲鱼搜索的要求;Elasticsearch是基于Lucene的准实时搜索引擎,也是比较常用的开源搜索引擎,但是其在算法扩展支撑/绝对实时的能力上与Ha3相差甚远;在同等硬件条件下,基于1200万数据做单机性能对比测试发现,Ha3比ElasticSearch开源系统的QPS高4倍,查询延迟低4倍;Elasticsearch在大规模数据量场景下的性能和稳定性与HA3相比尚有很大的差距。01闲鱼搜索体系运行流程下图是闲鱼搜索体系系统结构图,主要分在线和离线两个流程;索引构建流程索引构建即我们所谓的离线流程,其执行者BuildService①,负责将不同存储类型的纯文本商品数据构建成搜索引擎格式的索引文件。原始的商品数据有两类,一类是存放在存储上的全量商品数据,这个定期(一般以天为周期)通过DUMP②产出,另一类为实时变更的数据,在商品信息变更后,由业务系统即时同步给消息系统Swift③。最终分发给在线服务的Searcher④更新索引。搜索查询流程搜索查询即我们所谓的在线流程;闲鱼搜索服务应用A发起搜索请求,通过SP⑤进行服务能力编排;首先SP发起QP⑥算法服务调用,进行用户意图预测,并获取排序辅助信息;然后结合QP返回的结果加上业务系统的查询参数,向Ha3搜索引擎发起查询请求;Ha3搜索引擎QueryService⑦中Qrs⑧接收到查询请求后,分发给QueryService中的Searcher进行倒排索引召回、统计、条件过滤、文档打分及排序、摘要生成;最后Qrs将Searcher返回的结果进行整合后返回给SP,SP经过去重再返回给业务系统;02闲鱼搜索体系团队构成闲鱼搜索的运维体系,是一个相当复杂的构成;其中涉及很多团队的鼎力协作;首先必须有Ha3搜索引擎团队在底层提供核心的搜索引擎能力支持;主要负责Ha3搜索引擎核心能力的建设维护;提供并维护引擎运维操作平台和实时引擎搜索服务。然后是算法团队,在Ha3搜索引擎上进行定制,优化用户搜索体验;对闲鱼非结构化的商品通过算法模型进行理解,预测抽取出结构化信息,供搜索引擎商品索引使用;监控维护QP集群服务;开发并使用Ha3引擎排序插件,进行召回数据分桶实验,验证调优。最后是我们业务工程团队,串联整个搜索流程,监控维护整个搜索链路的可用性;主要维护搜索对接的数据,Ha3搜索引擎接入管理,进行SP搜索服务编排,制定合理的查询计划;以及闲鱼搜索统一在线查询服务的研发维护工作。本文亦是从业务工程团队的工作角度出发,阐述如何对复杂搜索业务系统进行稳定性的保障;稳定性治理01部署架构优化独立网关部署Ha3引擎通过SP提供基于HTTP协议的搜索服务API,对类似闲鱼这样复杂的搜索场景,每个闲鱼上层的业务如果都通过拼接SP HTTP接口参数的形式来使用搜索服务,所有上游业务都需要关心SP的拼接语法,会使开发成本剧增,而且如果由于特殊原因SP进行了语法调整或者不兼容升级,那么所有上层业务都需要修正逻辑,这样的设计不合理;为了让业务系统与搜索系统完全解耦,并且提高搜索服务的易用性,闲鱼搜索通过统一的业务搜索网关来提供简单一致的分布式服务,供闲鱼各上层搜索业务使用,并与SP对接,屏蔽掉SP对上游业务系统的穿透;最开始闲鱼搜索服务与其他很多不相关的业务场景共建在一个比较庞大的底层应用中;这种部署方式对稳定性要求很高的业务模块来说有非常大的安全隐患;1.各业务模块会相互影响;存在一定程度的代码耦合,同时还涉及机器资源的竞争,风险比较高;2.应用太过庞大,严重影响开发协作的效率和代码质量;于是将闲鱼搜索服务部署到独立的容器分组,新增应用A供闲鱼搜索服务专用,作为各业务使用搜索服务的独立网关,同时对接下游的SP搜索服务;保证服务是隔离和稳定的。前后部署图如下所示;多机房容灾部署在最初,闲鱼商品搜索服务对接的Ha3搜索引擎只部署在一个机房;当此机房出现比较严重的问题时,对上游业务影响非常大,甚至会引发故障;鉴于此,对闲鱼商品搜索引擎的在线离线集群进行双机房部署容灾;在详细展开之前,我们先大致理解下Ha3引擎DUMP流程的原理;如上图所示,Ha3引擎DUMP流程大致流程可以简单分为以下几步:准备源数据:评估业务需求,将需要接入引擎的数据准备好;一般业务数据大部分都是DB数据表,也有少数的ODPS⑨离线数据表;算法团队提供的数据绝大部分都是ODPS离线数据表;DUMP拉取数据:通过Ha3引擎团队提供的运维平台,可以将这些表的某些数据字段接入到创建好的搜索引擎中,后续DUMP执行的时候,Ha3离线引擎会拉取这些接入的表字段数据,形成一份引擎自用的镜像数据表,在这一步中,我们可以使用引擎团队提供的UDF工具,对数据进行清洗/过滤等处理;数据Merge:引擎将所有的镜像表数据,通过我们指定的主键进行Join;最终形成一份数据大宽表;供引擎创建索引使用;这一步数据Join后,还可以对最终的数据通过UDF进行进一步的清洗/过滤处理,验证通过的数据才会进入到大宽表;创建更新索引:Ha3离线引擎通过buildService,使用大宽表的数据,与事先我们在Ha3引擎运维平台指定好的索引Schame对齐,重新构建索引;以上流程可以通过Ha3引擎运维平台手动触发执行,执行完上述流程后,会生成一份新的索引;新的索引集群服务可用后,在线实时模块会将查询服务切换到新的索引集群上,完成一次索引的更新;这个完整流程我们将其称之为"全量";全量完成后,当系统有新的商品信息变动,且相应的数据表有启用实时更新(我们称之为增量功能,DB表是通过binlog/ODPS表是通过Swift消息通知的方式实现),则离线DUMP引擎会感知到此次变动,进而将相应的镜像数据表中商品数据更新,并会按上述离线DUMP流程中的步骤,将这个改动信息一直向引擎上层投递,直至成功更新引擎索引中的相应数据,或者中途被系统规则丢弃为止;这个实时数据更新的流程我们称之为"增量";增量更新还有一条通道:算法同学可以使用特殊的方式,通过Swift增量消息的方式直接将需要更新的数据不通过DUMP流程,直接更新到Ha3引擎索引中。闲鱼商品量飞速增长,目前已经达到数十亿;接入了数百的索引字段,由于闲鱼商品非结构化的原因,索引字段中只有一小部分供业务使用;另外大部分都是算法接入的索引,比如大量抽出来的标签数据,向量化数据等,这些向量化数据非常大;最终的情形表现为闲鱼商品搜索引擎的DUMP处理逻辑比较复杂,而且索引数据总量异常庞大,增量消息量也处在非常高的水位,再加上闲鱼商品单库存的现状;因此对数据更新的实时性要求非常高,这些都给稳定性带来了极大的制约。索引数据是搜索引擎的内容核心,如果进入引擎的索引数据有问题,或者新变更的数据没有更新到引擎索引中,将直接影响搜索服务的质量;搜索引擎单机房部署期间,时常会因为一些不稳定的因素,导致DUMP全量失败,或者增量延迟,甚至停止;一旦引擎DUMP出现问题,需要恢复基本都很困难,很多场景下甚至需要重新跑全量才能解决问题;但是闲鱼商品索引数据体量较大,做一次全量往往要大半天,没有办法快速止血,对业务造成了较大的影响;于是对搜索引擎进行双机房部署容灾(M/N机房),互为备份;两个离线DUMP机房采用相同的引擎配置和相同的数据源,产出相同的索引数据,分别供两个在线机房使用,两个机房的在线流量比例也可以按需实时调整;当M机房出现不可逆问题时,自动或手动将流量全部切换到N机房,实现线上快速止血,然后再按部就班排查解决M机房的问题。下图为最终的搜索机房部署情况;进行引擎双机房部署虽然增大了机器资源成本,但是除了上述业务容灾优点外,还有以下好处;引擎需求的发布,之前缺乏有效的灰度流程;当搜索引擎有重大变更/升级,出现高风险的发布时,可以先在单机房小流量beta测试,数据对比验证通过后,再发布到另一个机房;平常单机房能支撑全部搜索查询业务的流量,当遇到大促或大型活动时,将两个机房同时挂载提供服务,这样搜索服务能力和容量直接能翻倍;避免了单机房频繁扩缩容的困扰;性能评估时,可以单独对未承载流量的机房进行压测,即使由于压测导致宕机也不会对线上业务造成影响;02流量隔离上文独立网关部署一节中讲到,闲鱼搜索通过统一的业务搜索网关来提供简单一致的分布式服务,供闲鱼各上层搜索业务使用;使用统一的微服务,就必然带来上游不同业务优先级和可靠性保障的问题。闲鱼搜索服务支撑了种类繁多的上游业务,为了统一对各业务场景的流量/服务质量进行度量和管理,在上游业务接入闲鱼搜索服务时,需要申请使用相应的业务来源,这个业务来源标示会伴随着整个搜索查询的生命周期;在日志采集时直接使用,从而可以针对业务维度进行监控告警,实时感知业务运行的健康情况(简单监控视图如下图),也可以对具体业务进行流量管控,降级限流等;搜索业务来源生命周期图03分级监控体系对高稳定性系统,当出现问题,或者即将产生问题时,能即时感知,显得尤为重要;方便实时进行跟踪处理,防止继续扩大;目前使用的主要手段是建立健全完善的多维度监控告警体系;引擎基础服务监控使用监控可以快速发现问题,如果监控的粒度够细还能进行问题的快速定位;不过有时也会存在误报或者漏报的情况,因此真实的监控一定要结合每个业务自身系统的特性,梳理出关键链路,针对关键链路进行多维度360度无死角监控,并且进行合理的预警规则设置,监控预警才会比较有效;闲鱼搜索引擎在线离线流程/各上游重要应用系统的核心链路上,建立了完备的日志数据采集模块,对关键指标进行了精准的监控预警设置;做到任何问题都能及时被感知到。下图是搜索服务相应核心日志以及监控告警情况。模拟用户行为的在线业务监控上文提到,闲鱼搜索引擎索引体量比较大,需要很多团队共同协作,搜索流程复杂度比较高;而且有算法同学的加入,对闲鱼非结构化的商品做了很多AI识别,加上闲鱼都是单库存商品,对引擎实时性要求非常高;前面已经做了一些容灾的保障方案;但是对实时性的感知上还需要更进一步,才能及时知道数据的准确情况,是否存在更新延迟,以及延迟时间大概是多久等一系列健康度信息;解法是从业务层面进行实时性的监控告警;提取出闲鱼商品量比较大更新也比较频繁的类目K,在闲鱼的后台业务系统中,通过jkeins间隔一定时间(时间步长可以实时调整),使用类目K作为关键词和品类,根据商品更新时间索引降序招回,模拟用户轮询的方式发送搜索查询请求,召回满足要求的第一页商品;然后根据引擎召回数据的商品更新时间与当前系统时间进行差值比对,大于阈值时长(可以实时调整)说明存在较严重的数据更新延迟,则进行告警信息发送;04压测全链路压测对搜索服务以及各上游业务系统进行全链路压测改造;并使用线上真实的用户请求构造大批量的压测数据,在保证不影响线上业务正常进行的前提下,验证链路在超大流量模型下系统的容量和资源分配是否合理,找到链路中的性能瓶颈点,验证网络设备和集群容量。引擎单链路压测Ha3搜索引擎在线流程,支持通过回放线上高峰时段查询流量的方式,进行引擎在线服务性能压测。Ha3搜索引擎离线流程,支持通过回放一段时间Swift增量消息的方式,进行引擎DUMP增量性能压测。05灰度发布闲鱼商品的非结构化特性,离不开算法赋能;在我们的研发周期中,与两个算法团队,相当多的算法同学保持着深度合作;给闲鱼搜索带来了跨越式的发展,但是在团队协作和研发效率上也给我们带来了极大的挑战。算法团队、引擎团队、加上业务工程团队,非常大的搜索项目开发小组,每周都有非常多的新算法模型,新的引擎改造,新的业务模块需要上线。大量的新增逻辑改动直接上线,会带来很多问题;首先是代码层面,虽然预发环境有做充分测试,但也难保没有边缘逻辑存在测试遗漏的情况;即使预发测试都完全覆盖,但线上和预发终究环境不同,线上大流量环境及有可能会暴露一些隐藏的代码问题;第二方面,假使代码没有任何质量问题,但所有功能全部绑定上线,所有逻辑都混杂在一起,如何评定某个模块上线后的效果成为极大的困扰,特别是算法模型的优化,和业务上新模式的尝试,都需要根据详细的效果反馈数据指标来指导进行下一步的优化方向;因此急需一套灰度实验保障体系,不仅可以用来协调和隔离整个搜索业务中各个模块,做到对各模块进行单独的效果评估;并且还能提高大家的协作效率,让各模块能进行快速试错,快速迭代;为了解决以上非常重要的问题,业务工程团队开发了一套实验管理系统,用来进行搜索实验灰度调度管理,系统功能如上图所示;其具有以下特点。实验灵活方便,一个实验可以包含多个实验组件,一个实验组件可供多个实验使用;一个实验组件又可以包含多个实验分桶;各页面模块的实验都可以在系统中实时调控,包括实验的开/关;以及实验之间的关系处理;搜索实验埋点全链路打通,统计各种实验数据报表;统计数据接入了闲鱼门户和通天塔,可查看各个指标不同分桶的实验曲线;提升实验迭代速度,提升算法/业务效率,快速试错,加速搜索成交转化的增长;06应急预案根据评估分析或经验,对搜索服务中潜在的或可能发生的突发事件的关键点,事先制定好应急处置方案;当满足一定的条件时进行多维度多层级的自动降级限流,或者配置手动预案进行人工干预;任何时候发现线上问题,首先需要快速止血,避免问题的扩大;具有自动预案会自动发现问题,自动熔断,我们需要密切关注系统的运行情况,防止反弹;若出现反弹,并且对业务有较大影响时,快速人工介入执行降级预案;完成止血后再详细排查具体原因,当短时间无法确定问题根源时,如在问题出现时有过变更或发布,则第一时间回滚变更或发布。对系统中各级的依赖服务,熔断降级已经系统负载保护,我们使用的是阿里巴巴自主研发的资源调用控制组件Sentinel[4],目前已经开源;或者也可以使用Hytrix降级限流工具;07问题排查将闲鱼搜索链路接入阿里搜索问题排查平台,搜索实时查询请求的各个步骤输入的参数信息/产出的数据信息都会在此工具平台详细展示,方便各种问题的排查跟进,以及数据信息对比;可以对各查询条件下各个分桶的实验召回数据进行可视化显示,方便各个实验间的效果对比;以及每个召回商品的各类细节信息查看,包括业务数据和算法标签数据,还包含每个商品对应的各引擎插件算分情况,都能详细阅览;还可以根据商品Id,卖家Id,卖家Nick进行商品索引信息的披露;可以排查相应商品在引擎索引中的详细数据,如果数据和预想的有出入,具体是离线DUMP哪一步的处理逻辑导致的状态异常,都能一键查询。接入此问题排查平台后,能非常直观的掌握引擎的运行状况,搜索召回的链路状态;对快速发现问题根源,即时修复问题都有非常重大的作用!总结与展望本文主要介绍闲鱼如何保障复杂场景下搜索引擎服务的稳定性,主要从架构部署,隔离性,容量评估,风险感知&管控等方面进行阐述,介绍了如何稳定支撑20+线上搜索业务场景,做到了快速发现恢复线上问题,高效提前预知规避风险案例50+,极大程度提升了搜索服务的用户体验,保障了闲鱼搜索全年无故障;经过上述治理方案后,闲鱼搜索系统稳定性得到了极大的保障,同时我们也会继续深耕,在搜索能力的高可用、更易用上更进一步,让上游业务更加顺滑;希望给各位读者带来一些思考和启发。本文作者:元茂阅读原文本文来自云栖社区合作伙伴“阿里技术”,如需转载请联系原作者。

March 18, 2019 · 1 min · jiezi

阿里巴巴基于 Nacos 实现环境隔离的实践

随着Nacos 0.9版本的发布,Nacos 离正式生产版本(GA)又近了一步,其实已经有不少企业已经上了生产,例如虎牙直播。本周三(今天),晚上 19:00~21:00 将会在 Nacos 钉钉群(群号:21708933)直播 Nacos 1.0.0 所有发布特性的预览以及升级和使用上的指导。Nacos环境隔离通常,企业研发的流程是这样的:先在测试环境开发和测试功能,然后灰度,最后发布到生产环境。并且,为了生产环境的稳定,需要将测试环境和生产环境进行隔离,此时,必然会遇到问题是多环境问题,即:多个环境的数据如何隔离?如何优雅的隔离?(不需要用户做任何改动)本文将就 Nacos 环境隔离,向大家介绍阿里在这方面的实践经验。什么是环境?说到环境隔离,首先应该定义好什么是环境。环境这个词目前还没有一个比较统一的定义,有些公司叫环境,在阿里云上叫 region,在 Kubernetes 架构中叫 namespace。本文认为,环境是逻辑上或物理上独立的一整套系统,这套系统中包含了处理用户请求的全部组件,例如网关、服务框架、微服务注册中心、配置中心、消息系统、缓存、数据库等,可以处理指定类别的请求。举个例子,很多网站都会有用户 ID 的概念,可以按照用户 ID 划分,用户 ID 以偶数结尾的请求全部由一套系统处理,而奇数结尾的请求由另一套系统处理。如下图所示。 我们这里说的环境隔离是指物理隔离,即不同环境是指不同的机器集群。环境隔离有什么用上一节定义了环境的概念,即一套包含了处理用户请求全部必要组件的系统,用来处理指定类别的请求。本节跟大家讨论一下环境隔离有哪些好处。从概念的定义可以看出,环境隔离至少有三个方面的好处:故障隔离、故障恢复、灰度测试;故障隔离首先,因为环境是能够处理用户请求的独立组件单元,也就是说用户请求的处理链路有多长,都不会跳出指定的机器集群。即使这部分机器故障了,也只是会影响部分用户,从而把故障隔离在指定的范围内。如果我们按照用户id把全部机器分为十个环境,那么一个环境出问题,对用户的影响会降低为十分之一,大大提高系统可用性。故障恢复环境隔离的另一个重要优势是可以快速恢复故障。当某个环境的服务出现问题之后,可以快速通过下发配置,改变用户请求的路由方向,把请求路由到另一套环境,实现秒级故障恢复。当然,这需要一个强大的分布式系统支持,尤其是一个强大的配置中心(如Nacos),需要快速把路由规则配置数据推送到全网的应用进程。灰度测试灰度测试是研发流程中不可或缺的一个环节。传统的研发流程中,测试和灰度环节,需要测试同学做各种各样的配置,如绑定host、配置jvm参数、环境变量等等,比较麻烦。经过多年的实践,阿里巴巴内部的测试和灰度对开发和测试非常友好,通过环境隔离功能来保证请求在指定的机器集群处理,开发和测试不需要做任何做任何配置,大大提高了研发效率。Nacos如何做环境隔离前两节讲到了环境的概念和环境隔离的作用,本节介绍如何基于 Nacos,实现环境的隔离。Nacos 脱胎于阿里巴巴中间件部门的软负载小组,在环境隔离的实践过程中,我们是基于 Nacos 去隔离多个物理集群的,同时,在 Nacos 客户端不需要做任何代码改动的情况下,就可以实现环境的自动路由。开始前,我们先做一些约束:一台机器上部署的应用都在一个环境内;一个应用进程内默认情况下只连一个环境的 Nacos;通过某种手段可以拿到客户端所在机器 IP;用户对机器的网段有规划;基本原理是:网络中 32 位的 IPV4 可以划分为很多网段,如192.168.1.0/24,并且一般中大型的企业都会有网段规划,按照一定的用途划分网段。我们可以利用这个原理做环境隔离,即不同网段的 IP 属于不同的环境,如192.168.1.0/24属于环境A, 192.168.2.0/24属于环境B等。Nacos 有两种方式初始化客户端实例,一种是直接告诉客户端 Nacos 服务端的IP;另一种是告诉客户端一个 Endpoint,客户端通过 HTTP 请求到 Endpoint,查询 Nacos 服务端的 IP 列表。这里,我们利用第二种方式进行初始化。增强 Endpoint 的功能。在 Endpoint 端配置网段和环境的映射关系,Endpoint 在接收到客户端的请求后,根据客户端的来源 IP 所属网段,计算出该客户端的所属环境,然后找到对应环境的 IP 列表返回给客户端。如下图一个环境隔离server的示例上面讲了基于IP段做环境隔离的约束和基本原理,那么如何实现一个地址服务器呢。最简单的方法是基于nginx实现,利用nginx的geo模块,做IP端和环境的映射,然后利用nginx返回静态文件内容。安装nginx http://nginx.org/en/docs/install.html在nginx-proxy.conf中配置geo映射,参考这里geo $env { default “”; 192.168.1.0/24 -env-a; 192.168.2.0/24 -env-b;}配置nginx根路径及转发规则,这里只需要简单的返回静态文件的内容;# 在http模块中配置根路径root /tmp/htdocs;# 在server模块中配置location / { rewrite ^(.*)$ /$1$env break;}配置Nacos服务端IP列表配置文件,在/tmp/hotdocs/nacos目录下配置以环境名结尾的文件,文件内容为IP,一行一个$ll /tmp/hotdocs/nacos/total 0-rw-r–r– 1 user1 users 0 Mar 5 08:53 serverlist-rw-r–r– 1 user1 users 0 Mar 5 08:53 serverlist-env-a-rw-r–r– 1 user1 users 0 Mar 5 08:53 serverlist-env-b$cat /tmp/hotdocs/nacos/serverlist192.168.1.2192.168.1.3验证curl ’localhost:8080/nacos/serverlist'192.168.1.2192.168.1.3至此, 一个简单的根据IP网段做环境隔离的示例已经可以工作了,不同网段的nacos客户端会自动获取到不同的Nacos服务端IP列表,实现环境隔离。这种方法的好处是用户不需要配置任何参数,各个环境的代码和配置是一样的,但需要提供底层服务的同学做好网络规划和相关配置。总结本文简单介绍了环境隔离的概念,环境隔离的三个好处以及 Nacos 如何基于网段做环境隔离。最后,给出了一个基于 Nginx 做 Endpoint 服务端的环境隔离配置示例。需要注意的是,本文只是列出了一种可行的方法,不排除有更优雅的实现方法,如果大家有更好的方法,欢迎到Nacos 社区或官网贡献方案。本文作者:正己,GitHub ID @jianweiwang,负责 Nacos 的开发和社区维护,阿里巴巴高级开发工程师。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 14, 2019 · 1 min · jiezi

【Node】详解模块的实现过程

CommonJS 定义了 module、exports 和 require 模块规范,Node.js 为了实现这个简单的标准,从底层 C/C++ 到 JavaScript,从路径分析、文件定位到编译执行,经历了一系列复杂的过程。简单的了解 Node 模块的原理,有利于我们重新认识基于 Node 搭建的框架。一、CommonJS 模块规范CommonJS 规范或标准简单来说是一种理论,它期望 JavaScript 可以具备跨宿主环境执行的能力,不仅可以开发客户端应用,还可以开发服务端应用、命令行工具、桌面图形界面应用等。CommonJS 规范对模块的定义分为三个部分:模块定义在模块中存在 module 对象代表模块本身,模块上下文提供 exports 属性 ,将方法挂载在 exports 对象上即可以定义导出方式,例如: // math.js exports.add = function(){ //…}模块引用module 提供 require() 方法引入外部模块的API到当前的上下文中: var math = require(‘math’)模块标识模块标识实际就是传递给require()方法中的参数,可以是按小驼峰(camelCase)命名的字符串,也可以是文件路径。Node.js 借鉴了 CommonJS 规范的设计,特别是 CommonJS 的 Modules 规范,实现了一套模块系统,同时 NPM 实现了 CommonJS 的 Packages 规范,模块和包组成了 Node 应用开发的基础。二、Node 模块加载原理上述模块规范看起来十分简单,只有 module、exports 和 require,但 Node 是如何实现的呢?需要经历路径分析(模块的完整路径)、文件定位(文件扩展名或目录)、编译执行三个步骤。2.1 路径分析回顾require()接收 模块标识 作为参数来引入模块,Node 就是基于这个标识符进行路径分析。不同的标识符采用的分析方式是不同的,主要分为一下几类:Node 提供的核心模块,如 http、fs、path核心模块在 Node 源码编译时存为二进制执行文件,在 Node 启动时直接加载到内存中,路径分析中优先判断,所以加载速度很快,而且也不用后续的文件定位和编译执行。如果想加载与核心模块同名的自定义模块,如自定义http模块,那必须选用不同标志符或改用路径方式。路径形式的文件模块,.、..相对路径模块和/绝对路径模块以.、..或/开始的标识符都会当成文件模块处理,Node 会将require()中的路径转为真实路径作为索引,然后编译执行。由于文件模块明确了文件位置,所以缩短了路径分析时间,加载速度仅慢与核心模块。自定义模块,即非路径形式的文件模块即不是核心模块,也不是路径形式的文件模块,自定义文件是特殊的文件模块,在路径查找时Node会逐级查找该模块路径中的路径,模块路径查找策略示例如下:// paths.jsconsole.log(module.paths)// Terminal$ node paths.js[ ‘/Users/tong/WebstormProjects/testNode/node_modules’,’/Users/tong/WebstormProjects/node_modules’,’/Users/tong/node_modules’,’/Users/node_modules’,’/node_modules’ ]从上述示例输出的模块路径数组可以看出,模块的查找时沿当前路径向上逐级查找 node_modules 目录,直到目标路径为止,类似 JS 原型链或作用域链。路径越深速度越慢,所以自定义模块加载速度最慢。缓存优先机制:Node 会对引入过的模块进行缓存以提高性能,不同于浏览器缓存的是文件,Node 缓存的是编译和执行后的对象,所以require()对相同模块的二次加载采用缓存优先的方式。这个缓存优先是第一优先级的,比核心模块的优先级要高!2.2 文件定位模块路径分析完成后是文件定位,主要包括文件扩展名的分析、目录和包的处理。为了表达的更清晰,将文件定位分为四个步骤:step1: 补充扩展名通常require()中的标识符是不包含文件扩展名的,这种情况下,Node会按照.js、.json、.node 的顺序尝试补充扩展名。在尝试补充扩展名时,需要调用fs模块同步阻塞式判断文件是否存在,所以这里提升性能的小技巧,就是.json和.node文件传递给require()时带上扩展名会加快一些速度。step2: 目录处理查找 pakage.json如果补充扩展名后没有找到对应文件,但是得到了一个目录,此时Node会将目录当做一个包处理。依据CommonJS包规范的实现,Node会在目录下查找pakage.json(包描述文件),通过JSON.parse()解析成包描述对象,从中取main属性指定的文件名定位。step3: 继续默认查找 index 文件如果没有pakage.json或者main属性指定的文件名错误,那 Node 会将index当做默认文件名,依次查找 index.js、index.json、index.nodestep4: 进入下一个模块路径在上述目录分析过程中没有成功定位时,自定义模块按路径查找策略进入上一层 node_modules 目录,当整个模块路径数组遍历完毕后没有定位到文件,则会抛出查找失败异常。缓存加载的优化策略使得二次引入不需要路径分析、文件定位、编译执行这些过程,而且核心模块也不需要文件定位的过程,这大大提高了再次加载模块时的效率2.3 编译执行Node 中每个模块都是一个对象,在具体定位到文件后,Node 会新建该模块对象,然后根据路径载入并编译。不同的文件扩展名载入方法为:.js 文件: 通过 fs 模块同步读取后编译执行.json 文件: 通过 fs 模块同步读取后,用JSON.parse()解析并返回结果.node 文件: 这是用 C/C++ 写的扩展文件,通过process.dlopen()方法加载最后编译生成的其他扩展名: 都被当做 js 文件载入载入成功后 Node 会调用具体的编译方式将文件执行后返回给调用者。对于 .json 文件的编译最简单,JSON.parse()解析得到对象后直接赋值给模块对象的exports,而 .node 文件是C/C++编译生成的,Node 直接调用process.dlopen()载入执行就可以,下面重点介绍 .js 文件的编译:在 CommonJS 模块规范中有module、exports 和 require 这3个变量,在 Node API 文档中每个模块还有 __filename、__dirname这两个变量,但是在模块中没有定义这些变量,那它们是怎么产生的呢?事实上在编译过程中,Node 对每个 JS 文件都被进行了封装,例如一个 JS 文件会被封装成如下:(function (exports, require, module, __filename, __dirname) { var math = require(‘math’) export.add = function(){ //… }})首先每个模块文件之间都进行了作用域隔离,通过vm原生模块的runInThisContext()方法(类似 eval)返回一个具体的 function 对象,最后将当前模块对象的exports属性、require()方法、模块对象本身module、文件定位时得到的完整路径__filename和文件目录__dirname作为参数传递给这个 function 执行。模块的exports属性上的任何方法和属性都可以被外部调用,其余的则不可被调用。至此,module、exports 和 require的流程就介绍完了。曾经困惑过,每个模块都可以使用exports的情况下,为什么还必须用module.exports。这是因为exports在编译过程中时通过形参传入的,直接给exports形参赋值只改变形参的引用,不能改变作用域外的值,例如:let change = function (exports) { exports = 100 console.log(exports)}var exports = 2change(exports) // 100console.log(exports) // 2所以直接赋值给module.exports对象就不会改变形参的引用了。编译成功的模块会将文件路径作为索引缓存在 Module._cache 对象上,路径分析时优先查找缓存,提高二次引入的性能。三、Node 核心模块总结来说 Node 模块分为Node提供的核心模块和用户编写的文件模块。文件模块是在运行时动态加载,包括了上述完整的路径分析、文件定位、编译执行这些过程,核心模块在Node源码编译成可执行文件时存为二进制文件,直接加载在内存中,所以不用文件定位和编译执行。核心模块分为 C/C++ 编写的和 JavaScript 编写的两部分,在编译所有 C/C++ 文件之前,编译程序需要将所有的 JavaScript 核心模块编译为 C/C++ 可执行代码,编译成功的则放在 NativeModule._cache对象上,显然和文件模块 Module._cache的缓存位置不同。在核心模块中,有些模块由纯 C/C++ 编写的内建模块,主要提供 API 给 JavaScript 核心模块,通常不能被用户直接调用,而有些模块由 C/C++ 完成核心部分,而 JavaScript 实现封装和向外导出,如 buffer、fs、os 等。所以在Node的模块类型中存在依赖层级关系:内建模块(C/C++)—> 核心模块(JavaScript)—> 文件模块。使用require()十分的方便,但从 JavaScript 到 C/C++ 的过程十分复杂,总结来说需要经历 C/C++ 层面内建模块的定义、(JavaScript)核心模块的定义和引入以及(JavaScript)文件模块的引入。四、其它模块规范对比前后端的 JavaScript,浏览器端的 JavaScript 需要经历从同一个服务器端分发到多个客户端执行,通过网络加载代码,瓶颈在于宽带;而服务器端 JavaScript 相同代码需要多次执行,通过磁盘加载,瓶颈在于 CPU 和内存,所以前后端的 JavaScript 在 Http 两端的职责完全不用。Node 模块的引入几乎是同步的,而前端模块如果同步引入,那脚本加载需要太长的时间,所以 CommonJS 为后端 JavaScript 制定的规范不适合前端。而后出现 AMD 和 CMD 用于前端应用场景。AMD 规范AMD 即异步模块定义(Asynchronous Module Definition),模块定义为:define(id?, dependencies?, factory);AMD 模块需要用 define 明确定义一个模块,其中模块 id 与依赖 dependencies 是可选的,factory的内容就是实际代码的内容。例如指定一些依赖到模块中:define([‘dep1’, ‘dep2’], function(){ // module code});require.js 实现 AMD 规范的模块化,感兴趣的可以查看 require.js 的文档。CMD 规范CMD 模块的定义更加简单: define(factory);定义的模块同 Node 模块一样是隐式包装,在依赖部分支持动态引入,例如: define(function(require, exports, module){ // module code });require、exports、module 通过形参传递给模块,需要依赖模块时直接使用 require() 引入。sea.js 实现 AMD 规范的模块化,感兴趣的可以查看 sea.js 的文档。推荐两本 Node 的书籍:《Node.js 实战》主要是使用示例,《深入浅出 Node.js》偏实现原理。当然我的博客也会继续总结更新,下一篇内容会是关于 CommonJS 包规范和 NPM 包管理的内容。 ...

March 7, 2019 · 2 min · jiezi

分布式事务中间件 Fescar - 全局写排它锁解读

前言一般,数据库事务的隔离级别会被设置成 读已提交,已满足业务需求,这样对应在Fescar中的分支(本地)事务的隔离级别就是 读已提交,那么Fescar中对于全局事务的隔离级别又是什么呢?如果认真阅读了 分布式事务中间件Txc/Fescar-RM模块源码解读 的同学应该能推断出来:Fescar将全局事务的默认隔离定义成读未提交。对于读未提交隔离级别对业务的影响,想必大家都比较清楚,会读到脏数据,经典的就是银行转账例子,出现数据不一致的问题。而对于Fescar,如果没有采取任何其它技术手段,那会出现很严重的问题,比如:如上图所示,问最终全局事务A对资源R1应该回滚到哪种状态?很明显,如果再根据UndoLog去做回滚,就会发生严重问题:覆盖了全局事务B对资源R1的变更。那Fescar是如何解决这个问题呢?答案就是 Fescar的全局写排它锁解决方案,在全局事务A执行过程中全局事务B会因为获取不到全局锁而处于等待状态。对于Fescar的隔离级别,引用官方的一段话来作说明:全局事务的隔离性是建立在分支事务的本地隔离级别基础之上的。在数据库本地隔离级别 读已提交 或以上的前提下,Fescar 设计了由事务协调器维护的 全局写排他锁,来保证事务间的 写隔离,将全局事务默认定义在 读未提交 的隔离级别上。我们对隔离级别的共识是:绝大部分应用在 读已提交 的隔离级别下工作是没有问题的。而实际上,这当中又有绝大多数的应用场景,实际上工作在 读未提交 的隔离级别下同样没有问题。在极端场景下,应用如果需要达到全局的 读已提交,Fescar 也提供了相应的机制来达到目的。默认,Fescar 是工作在 读未提交 的隔离级别下,保证绝大多数场景的高效性。下面,本文将深入到源码层面对Fescar全局写排它锁实现方案进行解读。Fescar全局写排它锁实现方案在TC(Transaction Coordinator)模块维护,RM(Resource Manager)模块会在需要锁获取全局锁的地方请求TC模块以保证事务间的写隔离,下面就分成两个部分介绍:TC-全局写排它锁实现方案、RM-全局写排它锁使用一、TC—全局写排它锁实现方案首先看一下TC模块与外部交互的入口,下图是TC模块的main函数:上图中看出RpcServer处理通信协议相关逻辑,而对于TC模块真实处理器是DefaultCoordiantor,里面包含了所有TC对外暴露的功能,比如doGlobalBegin(全局事务创建)、doGlobalCommit(全局事务提交)、doGlobalRollback(全局事务回滚)、doBranchReport(分支事务状态上报)、doBranchRegister(分支事务注册)、doLockCheck(全局写排它锁校验)等,其中doBranchRegister、doLockCheck、doGlobalCommit就是全局写排它锁实现方案的入口。/*** 分支事务注册,在注册过程中会获取分支事务的全局锁资源*/@Overrideprotected void doBranchRegister(BranchRegisterRequest request, BranchRegisterResponse response, RpcContext rpcContext) throws TransactionException { response.setTransactionId(request.getTransactionId()); response.setBranchId(core.branchRegister(request.getBranchType(), request.getResourceId(), rpcContext.getClientId(), XID.generateXID(request.getTransactionId()), request.getLockKey()));}/*** 校验全局锁能否被获取到*/@Overrideprotected void doLockCheck(GlobalLockQueryRequest request, GlobalLockQueryResponse response, RpcContext rpcContext) throws TransactionException { response.setLockable(core.lockQuery(request.getBranchType(), request.getResourceId(), XID.generateXID(request.getTransactionId()), request.getLockKey()));}/*** 全局事务提交,会将全局事务下的所有分支事务的锁占用记录释放*/@Overrideprotected void doGlobalCommit(GlobalCommitRequest request, GlobalCommitResponse response, RpcContext rpcContext)throws TransactionException { response.setGlobalStatus(core.commit(XID.generateXID(request.getTransactionId())));}上述代码逻辑最后会被代理到DefualtCore去做执行如上图,不管是获取锁还是校验锁状态逻辑,最终都会被LockManger所接管,而LockManager的逻辑由DefaultLockManagerImpl实现,所有与全局写排它锁的设计都在DefaultLockManagerImpl中维护。首先,就先来看一下全局写排它锁的结构:private static final ConcurrentHashMap<String, ConcurrentHashMap<String, ConcurrentHashMap<Integer, Map<String, Long>>>> LOCK_MAP = new ConcurrentHashMap<~>();整体上,锁结构采用Map进行设计,前半段采用ConcurrentHashMap,后半段采用HashMap,最终其实就是做一个锁占用标记:在某个ResourceId(数据库源ID)上某个Tabel中的某个主键对应的行记录的全局写排它锁被哪个全局事务占用。下面,我们来看一下具体获取锁的源码:如上图注释,整个acquireLock逻辑还是很清晰的,对于分支事务需要的锁资源,要么是一次性全部成功获取,要么全部失败,不存在部分成功部分失败的情况。通过上面的解释,可能会有两个疑问:1. 为什么锁结构前半部分采用ConcurrentHashMap,后半部分采用HashMap?前半部分采用ConcurrentHashMap好理解:为了支持更好的并发处理;疑问的是后半部分为什么不直接采用ConcurrentHashMap,而采用HashMap呢?可能原因是因为后半部分需要去判断当前全局事务有没有占用PK对应的锁资源,是一个复合操作,即使采用ConcurrentHashMap还是避免不了要使用Synchronized加锁进行判断,还不如直接使用更轻量级的HashMap。2. 为什么BranchSession要存储持有的锁资源这个比较简单,在整个锁的结构中未体现分支事务占用了哪些锁记录,这样如果全局事务提交时,分支事务怎么去释放所占用的锁资源呢?所以在BranchSession保存了分支事务占用的锁资源。下图展示校验全局锁资源能否被获取逻辑:下图展示分支事务释放全局锁资源逻辑以上就是TC模块中全局写排它锁的实现原理:在分支事务注册时,RM会将当前分支事务所需要的锁资源一并传递过来,TC获取负责全局锁资源的获取(要么一次性全部成功,要么全部失败,不存在部分成功部分失败);在全局事务提交时,TC模块自动将全局事务下的所有分支事务持有的锁资源进行释放;同时,为减少全局写排它锁获取失败概率,TC模块对外暴露了校验锁资源能否被获取接口,RM模块可以在在适当位置加以校验,以减少分支事务注册时失败概率。二、RM-全局写排它锁使用在RM模块中,主要使用了TC模块全局锁的两个功能,一个是校验全局锁能否被获取,一个是分支事务注册去占用全局锁,全局锁释放跟RM无关,由TC模块在全局事务提交时自动释放。分支事务注册前,都会去做全局锁状态校验逻辑,以保证分支注册不会发生锁冲突。在执行Update、Insert、Delete语句时,都会在sql执行前后生成数据快照以组织成UndoLog,而生成快照的方式基本上都是采用Select…For Update形式,RM尝试校验全局锁能否被获取的逻辑就在执行该语句的执行器中:SelectForUpdateExecutor,具体如下图:基本逻辑如下:执行Select … For update语句,这样本地事务就占用了数据库对应行锁,其它本地事务由于无法抢占本地数据库行锁,进而也不会去抢占全局锁。循环掌握校验全局锁能否被获取,由于全局锁可能会被先于当前的全局事务获取,因此需要等之前的全局事务释放全局锁资源;如果这里校验能获取到全局锁,那么由于步骤1的原因,在当前本地事务结束前,其它本地事务是不会去获取全局锁的,进而保证了在当前本地事务提交前的分支事务注册不会因为全局锁冲突而失败。注:细心的同学可能会发现,对于Update、Delete语句对应的UpdateExecutor、DeleteExecutor中会因获取beforeImage而执行Select..For Update语句,进而会去校验全局锁资源状态,而对于Insert语句对应的InsertExecutor却没有相关全局锁校验逻辑,原因可能是:因为是Insert,那么对应插入行PK是新增的,全局锁资源必定未被占用,进而在本地事务提交前的分支事务注册时对应的全局锁资源肯定是能够获取得到的。接下来我们再来看看分支事务如何提交,对于分支事务中需要占用的全局锁资源如何生成和保存的。首先,在执行SQL完业务SQL后,会根据beforeImage和afterImage生成UndoLog,与此同时,当前本地事务所需要占用的全局锁资源标识也会一同生成,保存在ContentoionProxy的ConnectionContext中,如下图所示。在ContentoionProxy.commit中,分支事务注册时会将ConnectionProxy中的context内保存的需要占用的全局锁标识一同传递给TC进行全局锁的获取。以上,就是RM模块中对全局写排它锁的使用逻辑,因在真正执行获取全局锁资源前会去循环校验全局锁资源状态,保证在实际获取锁资源时不会因为锁冲突而失败,但这样其实坏处也很明显:在锁冲突比较严重时,会增加本地事务数据库锁占用时长,进而给业务接口带来一定的性能损耗。三、总结本文详细介绍了Fescar为在 读未提交 隔离级别下做到 写隔离 而实现的全局写排它锁,包括TC模块内的全局写排它锁的实现原理以及RM模块内如何对全局写排它锁的使用逻辑。在了解源码过程中,笔者也遗留了两个问题:1. 全局写排它锁数据结构保存在内存中,如果服务器重启/宕机了怎么办,即TC模块的高可用方案是什么呢?2. 一个Fescar管理的全局事务和一个非Fescar管理的本地事务之间发生锁冲突怎么办?具体问题如下图,问题是:全局事务A如何回滚?对于问题1有待继续研究;对于问题2目前已有答案,但Fescar目前暂未实现,具体就是全局事务A回滚时会报错,全局事务A内的分支事务A1回滚时会校验afterImage与当前表中对应行数据是否一致,如果一致才允许回滚,不一致则回滚失败并报警通知对应业务方,由业务方自行处理。参考Fescar官方介绍fescar锁设计和隔离级别的理解姊妹篇:分布式事务中间件TXC/Fescar—RM模块源码解读本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 27, 2019 · 1 min · jiezi

node遇上c++ --- 爱情来的太快(一) (没有文章)

文章后续更新,存入草稿时间长会被删, 希望谅解

February 24, 2019 · 1 min · jiezi

JavaScript 是如何工作的:模块的构建以及对应的打包工具

这是专门探索 JavaScript 及其所构建的组件的系列文章的第 20 篇。如果你错过了前面的章节,可以在这里找到它们:JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述!JavaScript 是如何工作的:深入V8引擎&编写优化代码的5个技巧!JavaScript 是如何工作的:内存管理+如何处理4个常见的内存泄漏!JavaScript 是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!JavaScript 是如何工作的:深入探索 websocket 和HTTP/2与SSE +如何选择正确的路径!JavaScript 是如何工作的:与 WebAssembly比较 及其使用场景!JavaScript 是如何工作的:Web Workers的构建块+ 5个使用他们的场景!JavaScript 是如何工作的:Service Worker 的生命周期及使用场景!JavaScript 是如何工作的:Web 推送通知的机制!JavaScript 是如何工作的:使用 MutationObserver 跟踪 DOM 的变化!JavaScript 是如何工作的:渲染引擎和优化其性能的技巧!JavaScript 是如何工作的:深入网络层 + 如何优化性能和安全!JavaScript 是如何工作的:CSS 和 JS 动画底层原理及如何优化它们的性能!JavaScript 是如何工作的:解析、抽象语法树(AST)+ 提升编译速度5个技巧!JavaScript 是如何工作的:深入类和继承内部原理+Babel和 TypeScript 之间转换!JavaScript 是如何工作的:存储引擎+如何选择合适的存储API!JavaScript 是如何工作的:Shadow DOM 的内部结构+如何编写独立的组件!JavaScript 是如何工作的:WebRTC 和对等网络的机制!JavaScript 是如何工作的:编写自己的 Web 开发框架 + React 及其虚拟 DOM 原理!如果你是 JavaScript 的新手,一些像 “module bundlers vs module loaders”、“Webpack vs Browserify” 和 “AMD vs.CommonJS” 这样的术语,很快让你不堪重负。JavaScript 模块系统可能令人生畏,但理解它对 Web 开发人员至关重要。在这篇文章中,我将以简单的言语(以及一些代码示例)为你解释这些术语。 希望这对你有会有帮助!什么是模块?好作者能将他们的书分成章节,优秀的程序员将他们的程序划分为模块。就像书中的章节一样,模块只是文字片段(或代码,视情况而定)的集群。然而,好的模块是高内聚低松耦的,具有不同的功能,允许在必要时对它们进行替换、删除或添加,而不会扰乱整体功能。为什么使用模块?使用模块有利于扩展、相互依赖的代码库,这有很多好处。在我看来,最重要的是:1)可维护性: 根据定义,模块是高内聚的。一个设计良好的模块旨在尽可能减少对代码库部分的依赖,这样它就可以独立地增强和改进,当模块与其他代码片段解耦时,更新单个模块要容易得多。回到我们的书的例子,如果你想要更新你书中的一个章节,如果对一个章节的小改动需要你调整每一个章节,那将是一场噩梦。相反,你希望以这样一种方式编写每一章,即可以在不影响其他章节的情况下进行改进。2)命名空间: 在 JavaScript 中,顶级函数范围之外的变量是全局的(这意味着每个人都可以访问它们)。因此,“名称空间污染”很常见,完全不相关的代码共享全局变量。在不相关的代码之间共享全局变量在开发中是一个大禁忌。正如我们将在本文后面看到的,通过为变量创建私有空间,模块允许我们避免名称空间污染。3)可重用性:坦白地说:我们将前写过的代码复制到新项目中。 例如,假设你从之前项目编写的一些实用程序方法复制到当前项目中。这一切都很好,但如果你找到一个更好的方法来编写代码的某些部分,那么你必须记得回去在曾经使用过的其他项目更新它。这显然是在浪费时间。如果有一个我们可以一遍又一遍地重复使用的模块,不是更容易吗?如何创建模块?有多种方法来创建模块,来看几个:模块模式模块模式用于模拟类的概念(因为 JavaScript 本身不支持类),因此我们可以在单个对象中存储公共和私有方法和变量——类似于在 Java 或 Python 等其他编程语言中使用类的方式。这允许我们为想要公开的方法创建一个面向公共的 API,同时仍然将私有变量和方法封装在闭包范围中。有几种方法可以实现模块模式。在第一个示例中,将使用匿名闭包,将所有代码放在匿名函数中来帮助我们实现目标。(记住:在 JavaScript 中,函数是创建新作用域的唯一方法。)例一:匿名闭包(function () { // 将这些变量放在闭包范围内实现私有化 var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return ‘平均分 ’ + total / myGrades.length + ‘.’; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return ‘挂机科了 ’ + failingGrades.length + ’ 次。’; } console.log(failing()); // 挂机科了次}());使用这个结构,匿名函数就有了自己的执行环境或“闭包”,然后我们立即执行。这让我们可以从父(全局)命名空间隐藏变量。这种方法的优点是,你可以在这个函数中使用局部变量,而不会意外地覆盖现有的全局变量,但仍然可以访问全局变量,就像这样: var global = ‘你好,我是一个全局变量。)’; (function () { // 将这些变量放在闭包范围内实现私有化 var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return ‘平均分 ’ + total / myGrades.length + ‘.’; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return ‘挂机科了 ’ + failingGrades.length + ’ 次。’; } console.log(failing()); // 挂机科了次 onsole.log(global); // 你好,我是一个全局变量。 }());注意,匿名函数的圆括号是必需的,因为以关键字 function 开头的语句通常被认为是函数声明(请记住,JavaScript 中不能使用未命名的函数声明)。因此,周围的括号将创建一个函数表达式,并立即执行这个函数,这还有另一种叫法 立即执行函数(IIFE)。如果你对这感兴趣,可以在这里了解到更多。例二:全局导入jQuery 等库使用的另一种流行方法是全局导入。它类似于我们刚才看到的匿名闭包,只是现在我们作为参数传入全局变量:(function (globalVariable) { // 在这个闭包范围内保持变量的私有化 var privateFunction = function() { console.log(‘Shhhh, this is private!’); } // 通过 globalVariable 接口公开下面的方法 // 同时将方法的实现隐藏在 function() 块中 globalVariable.each = function(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } }; globalVariable.filter = function(collection, test) { var filtered = []; globalVariable.each(collection, function(item) { if (test(item)) { filtered.push(item); } }); return filtered; }; globalVariable.map = function(collection, iterator) { var mapped = []; globalUtils.each(collection, function(value, key, collection) { mapped.push(iterator(value)); }); return mapped; }; globalVariable.reduce = function(collection, iterator, accumulator) { var startingValueMissing = accumulator === undefined; globalVariable.each(collection, function(item) { if(startingValueMissing) { accumulator = item; startingValueMissing = false; } else { accumulator = iterator(accumulator, item); } }); return accumulator; }; }(globalVariable));在这个例子中,globalVariable 是唯一的全局变量。与匿名闭包相比,这种方法的好处是可以预先声明全局变量,使得别人更容易阅读代码。例三:对象接口另一种方法是使用立即执行函数接口对象创建模块,如下所示:var myGradesCalculate = (function () { // 将这些变量放在闭包范围内实现私有化 var myGrades = [93, 95, 88, 0, 55, 91]; // 通过接口公开这些函数,同时将模块的实现隐藏在function()块中 return { average: function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return’平均分 ’ + total / myGrades.length + ‘.’; }, failing: function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return ‘挂科了’ + failingGrades.length + ’ 次.’; } }})();myGradesCalculate.failing(); // ‘挂科了 2 次.’ myGradesCalculate.average(); // ‘平均分 70.33333333333333.‘正如您所看到的,这种方法允许我们通过将它们放在 return 语句中(例如算平均分和挂科数方法)来决定我们想要保留的变量/方法(例如 myGrades)以及我们想要公开的变量/方法。例四:显式模块模式这与上面的方法非常相似,只是它确保所有方法和变量在显式公开之前都是私有的:var myGradesCalculate = (function () { // 将这些变量放在闭包范围内实现私有化 var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return’平均分 ’ + total / myGrades.length + ‘.’; }; var failing = function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return ‘挂科了’ + failingGrades.length + ’ 次.’; }; // Explicitly reveal public pointers to the private functions // that we want to reveal publicly return { average: average, failing: failing }})();myGradesCalculate.failing(); // ‘挂科了 2 次.’ myGradesCalculate.average(); // ‘平均分 70.33333333333333.‘这可能看起来很多,但它只是模块模式的冰山一角。 以下是我在自己的探索中发现有用的一些资源:Learning JavaScript Design Patterns:作者是 Addy Osmani,一本简洁又令人印象深刻的书籍,蕴藏着许多宝藏。Adequately Good by Ben Cherry:包含模块模式的高级用法示例。Blog of Carl Danley:模块模式概览,也是 JavaScript 许多设计模式的资源库。CommonJS 和 AMD所有这些方法都有一个共同点:使用单个全局变量将其代码包装在函数中,从而使用闭包作用域为自己创建一个私有名称空间。虽然每种方法都有效且都有各自特点,但却都有缺点。首先,作为开发人员,你需要知道加载文件的正确依赖顺序。例如,假设你在项目中使用 Backbone,因此你可以将 Backbone 的源代码 以<script> 脚本标签的形式引入到文件中。但是,由于 Backbone 对 Underscore.js 有很强的依赖性,因此 Backbone 文件的脚本标记不能放在Underscore.js 文件之前。作为一名开发人员,管理依赖关系并正确处理这些事情有时会令人头痛。另一个缺点是它们仍然会导致名称空间冲突。例如,如果两个模块具有相同的名称怎么办?或者,如果有一个模块的两个版本,并且两者都需要,该怎么办?幸运的是,答案是肯定的。有两种流行且实用的方法:CommonJS 和 AMD。CommonJSCommonJS 是一个志愿者工作组,负责设计和实现用于声明模块的 JavaScript API。CommonJS 模块本质上是一个可重用的 JavaScript,它导出特定的对象,使其可供其程序中需要的其他模块使用。 如果你已经使用 Node.js 编程,那么你应该非常熟悉这种格式。使用 CommonJS,每个 JavaScript 文件都将模块存储在自己独立的模块上下文中(就像将其封装在闭包中一样)。 在此范围内,我们使用 module.exports 导出模块,或使用 require 来导入模块。在定义 CommonJS 模块时,它可能是这样的:function myModule() { this.hello = function() { return ‘hello!’; } this.goodbye = function() { return ‘goodbye!’; }}module.exports = myModule;我们使用特殊的对象模块,并将函数的引用放入 module.exports 中。这让 CommonJS 模块系统知道我们想要公开什么,以便其他文件可以使用它。如果想使用 myModule,只需要使用 require 方法就可以,如下:var myModule = require(‘myModule’);var myModuleInstance = new myModule();myModuleInstance.hello(); // ‘hello!‘myModuleInstance.goodbye(); // ‘goodbye!‘与前面讨论的模块模式相比,这种方法有两个明显的好处:避免全局命名空间污染依赖关系更加明确另外需要注意的是,CommonJS 采用服务器优先方法并同步加载模块。 这很重要,因为如果我们需要三个其他模块,它将逐个加载它们。现在,它在服务器上运行良好,但遗憾的是,在为浏览器编写 JavaScript 时使用起来更加困难。 可以这么说,从网上读取模块比从磁盘读取需要更长的时间。 只要加载模块的脚本正在运行,它就会阻止浏览器运行其他任何内容,直到完成加载,这是因为 JavaScript 是单线程且 CommonJS 是同步加载的。AMDCommonJS一切都很好,但是如果我们想要异步加载模块呢? 答案是 异步模块定义,简称 AMD。使用 AMD 的加载模块如下:define([‘myModule’, ‘myOtherModule’], function(myModule, myOtherModule) { console.log(myModule.hello());});define 函数的第一个参数是一个数组,数组中是依赖的各种模块。这些依赖模块在后台(以非阻塞的方式)加载进来,一旦加载完毕,define 函数就会调用第二个参数,即回调函数执行操作。接下来,回调函数接收参数,即依赖模块 - 示例中就是 myModule 和 myOtherModule - 允许函数使用这些依赖项, 最后,所依赖的模块本身也必须使用 define 关键字来定义。例如,myModule如下所示:define([], function() { return { hello: function() { console.log(‘hello’); }, goodbye: function() { console.log(‘goodbye’); } };});因此,与 CommonJS 不同,AMD 采用浏览器优先的方法和异步行为来完成工作。 (注意,有很多人坚信在开始运行代码时动态加载文件是不利的,我们将在下一节关于模块构建的内容中探讨更多内容)。除了异步性,AMD 的另一个好处是模块可以是对象,函数,构造函数,字符串,JSON 和许多其他类型,而CommonJS 只支持对象作为模块。也就是说,和CommonJS相比,AMD不兼容io、文件系统或者其他服务器端的功能特性,而且函数包装语法与简单的require 语句相比有点冗长。UMD对于同时支持 AMD 和 CommonJS 特性的项目,还有另一种格式:通用模块定义(Universal Module Definition, UMD)。UMD 本质上创造了一种使用两者之一的方法,同时也支持全局变量定义。因此,UMD 模块能够同时在客户端和服务端同时工作。简单看一下 UMD 是怎样工作的:(function (root, factory) { if (typeof define === ‘function’ && define.amd) { // AMD define([‘myModule’, ‘myOtherModule’], factory); } else if (typeof exports === ‘object’) { // CommonJS module.exports = factory(require(‘myModule’), require(‘myOtherModule’)); } else { // Browser globals (Note: root is window) root.returnExports = factory(root.myModule, root.myOtherModule); }}(this, function (myModule, myOtherModule) { // Methods function notHelloOrGoodbye(){}; // A private method function hello(){}; // A public method because it’s returned (see below) function goodbye(){}; // A public method because it’s returned (see below) // Exposed public methods return { hello: hello, goodbye: goodbye }}));Github 上 enlightening repo 里有更多关于 UMD 的例子。Native JS你可能已经注意到,上面的模块都不是 JavaScript 原生的。相反,我们已经创建了通过使用模块模式、CommonJS 或 AMD 来模拟模块系统的方法。幸运的是,TC39(定义 ECMAScript 的语法和语义的标准组织)一帮聪明的人已经引入了ECMAScript 6(ES6)的内置模块。ES6 为导入导出模块提供了很多不同的可能性,已经有许多其他人花时间解释这些,下面是一些有用的资源:jsmodules.ioexploringjs.com与 CommonJS 或 AMD 相比,ES6 模块最大的优点在于它能够同时提供两方面的优势:简明的声明式语法和异步加载,以及对循环依赖项的更好支持。也许我个人最喜欢的 ES6 模块功能是它的导入模块是导出时模块的实时只读视图。(相比起 CommonJS,导入的是导出模块的拷贝副本,因此也不是实时的)。下面是一个例子:// lib/counter.jsvar counter = 1;function increment() { counter++;}function decrement() { counter–;}module.exports = { counter: counter, increment: increment, decrement: decrement};// src/main.jsvar counter = require(’../../lib/counter’);counter.increment();console.log(counter.counter); // 1在这个例子中,我们基本上创建了两个模块的对象:一个用于导出它,一个在我们需要的时候引入。此外,在 main.js 中的对象目前是与原始模块是相互独立的,这就是为什么即使我们执行 increment 方法,它仍然返回 1,因为引入的变量和最初导入的变量是毫无关联的。需要改变你引入的对象唯一的方式是手动执行增加:counter.counter++;console.log(counter.counter); // 2另一方面,ES6创建了我们导入的模块的实时只读视图:// lib/counter.jsexport let counter = 1;export function increment() { counter++;}export function decrement() { counter–;}// src/main.jsimport * as counter from ‘../../counter’;console.log(counter.counter); // 1counter.increment();console.log(counter.counter); // 2超酷?我发现这一点是因为ES6允许你可以把你定义的模块拆分成更小的模块而不用删减功能,然后你还能反过来把它们合成到一起, 完全没问题。什么是模块打包?总体上看,模块打包只是将一组模块(及其依赖项)以正确的顺序拼接到一个文件(或一组文件)中的过程。正如 Web开发的其它方方面面,棘手的问题总是潜藏在具体的细节里。为什么需要打包?将程序划分为模块时,通常会将这些模块组织到不同的文件和文件夹中。 有可能,你还有一组用于正在使用的库的模块,如 Underscore 或 React。因此,每个文件都必须以一个 <script> 标签引入到主 HTML 文件中,然后当用户访问你的主页时由浏览器加载进来。 每个文件使用 <script> 标签引入,意味着浏览器不得不分别逐个的加载它们。这对于页面加载时间来说简直是噩梦。为了解决这个问题,我们将所有文件打包或“拼接”到一个大文件(或视情况而定的几个文件),以减少请求的数量。 当你听到开发人员谈论“构建步骤”或“构建过程”时,这就是他们所谈论的内容。另一种加速构建操作的常用方法是“缩减”打包代码。 缩减是从源代码中移除不必要的字符(例如,空格,注释,换行符等)的过程,以便在不改变代码功能的情况下减少内容的整体大小。较少的数据意味着浏览器处理时间会更快,从而减少了下载文件所需的时间。 如果你见过具有 “min” 扩展名的文件,如 “underscore-min.js” ,可能会注意到与完整版相比,缩小版本非常小(不过很难阅读)。除了捆绑和/或加载模块之外,模块捆绑器还提供了许多其他功能,例如在进行更改时生成自动重新编译代码或生成用于调试的源映射。构建工具(如 Gulp 和 Grunt)能为开发者直接进行拼接和缩减,确保为开发人员提供可读代码,同时有利于浏览器执行的代码。打包模块有哪些不同的方法?当你使用一种标准模块模式(上部分讨论过)来定义模块时,拼接和缩减文件非常有用。 你真正在做的就是将一堆普通的 JavaScript 代码捆绑在一起。但是,如果你坚持使用浏览器无法解析的非原生模块系统(如 CommonJS 或 AMD(甚至是原生 ES6模块格式)),则需要使用专门工具将模块转换为排列正确、浏览器可解析的代码。 这就是 Browserify,RequireJS,Webpack 和其他“模块打包工具”或“模块加载工具”的用武之地。除了打包和/或加载模块之外,模块打包器还提供了许多其他功能,例如在进行更改时生成自动重新编译代码或生成用于调试的源映射。下面是一些常见的模块打包方法:打包 CommonJS正如前面所知道的,CommonJS以同步方式加载模块,这没有什么问题,只是它对浏览器不实用。我提到过有一个解决方案——其中一个是一个名为 Browserify 的模块打包工具。Browserify 是一个为浏览器编译 CommonJS模块的工具。例如,有个 main.js 文件,它导入一个模块来计算一组数字的平均值:var myDependency = require(‘myDependency’);var myGrades = [93, 95, 88, 0, 91];var myAverageGrade = myDependency.average(myGrades);在这种情况下,我们有一个依赖项(myDependency),使用下面的命令,Browserify 以 main.js 为入口把所有依赖的模块递归打包成一个文件:browserify main.js -o bundle.jsBrowserify 通过跳入文件分析每一个依赖的 抽象语法树(AST),以便遍历项目的整个依赖关系图。一旦确定了依赖项的结构,就把它们按正确的顺序打包到一个文件中。然后,在 html 里插入一个用于引入 “bundle.js” 的 <script> 标签,从而确保你的源代码在一个 HTTP 请求中完成下载。类似地,如果有多个文件且有多个依赖时,只需告诉 Browserify 的入口文件路径即可。最后打包后的文件可以通过 Minify-JS 之类的工具压缩打包后的代码。打包 AMD如果你正在使用 AMD,你需要使用像 RequireJS 或者 Curl 这样的 AMD 加载器。模块加载器(与模块打包工具不同)会动态加载程序需要运行的模块。提醒一下,AMD 与 CommonJS 的主要区别之一是它以异步方式加载模块。 从这个意义上说,对于 AMD,从技术上讲,实际上并不需要构建步骤,因为异步加载模块意味着在运行过程中逐步下载那些程序所需要的文件,而不是用户刚进入页面就一下把所有文件都下载下来。但实际上,对于每个用户操作而言,随着时间的推移,大容量请求的开销在生产中没有多大意义。 大多数 Web 开发人员仍然使用构建工具打包和压缩 AMD 模块以获得最佳性能,例如使用 RequireJS 优化器,r.js 等工具。总的来说,AMD 和 CommonJS 在打包方面的区别在于:在开发期间,AMD 可以省去任何构建过程。当然,在代码上线前,要使用优化工具(如 r.js)进行优化。Webpack就打包工具而言,Webpack 是一个新事物。它被设计成与你使用的模块系统无关,允许开发人员在适当的情况下使用 CommonJS、AMD 或 ES6。你可能想知道,为什么我们需要 Webpack,而我们已经有了其他打包工具了,比如 Browserify 和 RequireJS,它们可以完成工作,并且做得非常好。首先,Webpack 提供了一些有用的特性,比如 “代码分割”(code splitting) —— 一种将代码库分割为“块(chunks)”的方式,从而能实现按需加载。例如,如果你的 Web 应用程序,其中只需要某些代码,那么将整个代码库都打包进一个大文件就不是很高效。 在这种情况下,可以使用代码分割,将需要的部分代码抽离在"打包块",在执行按需加载,从而避免在最开始就遇到大量负载的麻烦。代码分割只是 Webpack 提供的众多引人注目的特性之一,网上有很多关于 “Webpack 与 Browserify 谁更好” 的激烈讨论。以下是一些客观冷静的讨论,帮助我稍微理清了头绪:https://gist.github.com/subst…http://mattdesl.svbtle.com/br...http://blog.namangoel.com/bro...ES6 模块当前 JS 模块规范(CommonJS, AMD) 与 ES6 模块之间最重要的区别是 ES6 模块的设计考虑到了静态分析。这意味着当你导入模块时,导入的模块在编译阶段也就是代码开始运行之前就被解析了。这允许我们在运行程序之前移,移除那些在导出模块中不被其它模块使用的部分。移除不被使用的模块能节省空间,且有效地减少浏览器的压力。一个常见的问题,使用一些工具,如 Uglify.js ,缩减代码时,有一个死码删除的处理,它和 ES6 移除没用的模块又有什么不同呢?只能说 “视情况而定”。死码消除(Dead codeelimination)是一种编译器原理中编译最优化技术,它的用途是移除对程序运行结果没有任何影响的代码。移除这类的代码有两种优点,不但可以减少程序的大小,还可以避免程序在运行中进行不相关的运算行为,减少它运行的时间。不会被运行到的代码(unreachable code)以及只会影响到无关程序运行结果的变量(Dead Variables),都是死码(Dead code)的范畴。有时,在 UglifyJS 和 ES6 模块之间死码消除的工作方式完全相同,有时则不然。如果你想验证一下, Rollup’s wiki 里有个很好的示例。ES6 模块的不同之处在于死码消除的不同方法,称为“tree shaking”。“tree shaking” 本质上是死码消除反过程。它只包含包需要运行的代码,而非排除不需要的代码。来看个例子:假设有一个带有多个函数的 utils.js 文件,每个函数都用 ES6 的语法导出:export function each(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } }export function filter(collection, test) { var filtered = []; each(collection, function(item) { if (test(item)) { filtered.push(item); } }); return filtered;}export function map(collection, iterator) { var mapped = []; each(collection, function(value, key, collection) { mapped.push(iterator(value)); }); return mapped;}export function reduce(collection, iterator, accumulator) { var startingValueMissing = accumulator === undefined; each(collection, function(item) { if(startingValueMissing) { accumulator = item; startingValueMissing = false; } else { accumulator = iterator(accumulator, item); } }); return accumulator;}接着,假设我们不知道要在程序中使用什么 utils.js 中的哪个函数,所以我们将上述的所有模块导入main.js中,如下所示:import * as Utils from ‘./utils.js’;最终,我们只用到的 each 方法:import * as Utils from ‘./utils.js’;Utils.each([1, 2, 3], function(x) { console.log(x) });“tree shaken” 版本的 main.js 看起来如下(一旦模块被加载后):function each(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } };each([1, 2, 3], function(x) { console.log(x) });注意:只导出我们使用的 each 函数。同时,如果决定使用 filte r函数而不是每个函数,最终会看到如下的结果:import * as Utils from ‘./utils.js’;Utils.filter([1, 2, 3], function(x) { return x === 2 });tree shaken 版本如下:function each(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } };function filter(collection, test) { var filtered = []; each(collection, function(item) { if (test(item)) { filtered.push(item); } }); return filtered;};filter([1, 2, 3], function(x) { return x === 2 });此时,each 和 filter 函数都被包含进来。这是因为 filter 在定义时使用了 each。因此也需要导出该函数模块以保证程序正常运行。构建 ES6 模块我们知道 ES6 模块的加载方式与其他模块格式不同,但我们仍然没有讨论使用 ES6 模块时的构建步骤。遗憾的是,因为浏览器对 ES6模 块的原生支持还不够完善,所以现阶段还需要我们做一些补充工作。下面是几个在浏览器中 构建/转换 ES6 模块的方法,其中第一个是目前最常用的方法:使用转换器(例如 Babel 或 Traceur)以 CommonJS、AMD 或 UMD 格式将 ES6 代码转换为 ES5 代码,然后再通过 Browserify 或 Webpack 一类的构建工具来进行构建。使用 Rollup.js,这其实和上面差不多,只是 Rollup 捎带 ES6 模块的功能,在打包之前静态分析ES6 代码和依赖项。 它利用 “tree shaking” 技术来优化你的代码。 总言,当您使用ES6模块时,Rollup.js 相对于 Browserify 或 Webpack 的主要好处是 tree shaking 能让打包文件更小。 需要注意的是,Rollup提 供了几种格式来的打包代码,包括 ES6,CommonJS,AMD,UMD 或 IIFE。 IIFE 和 UMD 捆绑包可以直接在浏览器中工作,但如果你选择打包 AMD,CommonJS 或 ES6,需需要寻找能将代码转成浏览器能理解运行的代码的方法(例如,使用 Browserify, Webpack,RequireJS等)。小心踩坑作为 web 开发人员,我们必须经历很多困难。转换语法优雅的ES6代码以便在浏览器里运行并不总是容易的。问题是,什么时候 ES6 模块可以在浏览器中运行而不需要这些开销?答案是:“尽快”。ECMAScript 目前有一个解决方案的规范,称为 ECMAScript 6 module loader API。简而言之,这是一个纲领性的、基于 Promise 的 API,它支持动态加载模块并缓存它们,以便后续导入不会重新加载模块的新版本。它看起来如下:// myModule.jsexport class myModule { constructor() { console.log(‘Hello, I am a module’); } hello() { console.log(‘hello!’); } goodbye() { console.log(‘goodbye!’); }} // main.jsSystem.import(‘myModule’).then(function(myModule) { new myModule.hello();});// ‘hello!’你亦可直接对 script 标签指定 “type=module” 来定义模块,如:<script type=“module”> // loads the ‘myModule’ export from ‘mymodule.js’ import { hello } from ‘mymodule’; new Hello(); // ‘Hello, I am a module!’</script>更加详细的介绍也可以在 Github 上查看:es-module-loader此外,如果您想测试这种方法,请查看 SystemJS,它建立在 ES6 Module Loader polyfill 之上。 SystemJS 在浏览器和 Node 中动态加载任何模块格式(ES6模块,AMD,CommonJS 或 全局脚本)。 它跟踪“模块注册表”中所有已加载的模块,以避免重新加载先前已加载过的模块。 更不用说它还会自动转换ES6模块(如果只是设置一个选项)并且能够从任何其他类型加载任何模块类型!有了原生的 ES6 模块后,还需要模块打包吗?对于日益普及的 ES6 模块,下面有一些有趣的观点:HTTP/2 会让模块打包过时吗?对于 HTTP/1,每个TCP连接只允许一个请求。这就是为什么加载多个资源需要多个请求。有了 HTTP/2,一切都变了。HTTP/2 是完全多路复用的,这意味着多个请求和响应可以并行发生。因此,我们可以在一个连接上同时处理多个请求。由于每个 HTTP 请求的成本明显低于HTTP/1,因此从长远来看,加载一组模块不会造成很大的性能问题。一些人认为这意味着模块打包不再是必要的,这当然有可能,但这要具体情况具体分析了。例如,模块打包还有 HTTP/2 没有好处,比如移除冗余的导出模块以节省空间。 如果你正在构建一个性能至关重要的网站,那么从长远来看,打包可能会为你带来增量优势。 也就是说,如果你的性能需求不是那么极端,那么通过完全跳过构建步骤,可以以最小的成本节省时间。总的来说,绝大多数网站都用上 HTTP/2 的那个时候离我们现在还很远。我预测构建过程将会保留,至少在近期内。CommonJS、AMD 与 UMD 会被淘汰吗?一旦 ES6 成为模块标准,我们还需要其他非原生模块规范吗?我觉得还有。Web 开发遵守一个标准方法进行导入和导出模块,而不需要中间构建步骤——网页开发长期受益于此。但 ES6 成为模块规范需要多长时间呢?机会是有,但得等一段时间 。再者,众口难调,所以“一个标准的方法”可能永远不会成为现实。总结希望这篇文章能帮你理清一些开发者口中的模块和模块打包的相关概念,共进步。原文:https://medium.freecodecamp.o…https://medium.freecodecamp.o…你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

February 17, 2019 · 7 min · jiezi

PHPUnit实践三(构建模块化的测试单元)

本系列教程所有的PHPUnit测试基于PHPUnit6.5.9版本,Lumen 5.5框架目录结构模块下的目录是符合Lumen的模块结构的如:Controllers、Models、Logics等是Lumen模块目录下的结构目录如果有自己的目录同级分配即可,如我这里的Requests整体结构├── BaseCase.php 重写过Lumen基类的测试基类,用于我们用这个基类做测试基类,后续会说明├── bootstrap.php tests自动加载文件├── Cases 测试用例目录│ └── Headline 某测试模块│ ├── logs 日志输出目录│ ├── PipeTest.php PHPUnit流程测试用例│ ├── phpunit.xml phpunit配置文件xml│ └── README.md 本模块测试用例说明├── ExampleTest.php 最原始测试demo└── TestCase.php Lumen自带的测试基类某模块的目录结构Headline //某测试模块测试用例目录├── Cache├── Controllers│ ├── ArticleTest.php│ ├── …├── Listeners│ └── MyListener.php├── Logics├── Models│ ├── ArticleTest.php│ ├── …├── README.md├── Requests│ ├── ArticleTest.php│ ├── …├── logs //日志和覆盖率目录│ ├── html│ │ ├── …│ │ └── index.html│ ├── logfile.xml│ ├── testdox.html│ └── testdox.txt├── phpunit-debug-demo.xml //phpunit.xml案例├── phpunit-debug.xml //改名后测试用的└── phpunit.xml //正式用的xml配置BaseCase.php<?phpnamespace Test;use Illuminate\Database\Eloquent\Factory;class BaseCase extends TestCase{ protected $seeder = false; const DOMAIN = “http://xxx.com”; const API_URI = []; const TOKEN = [ ’local’ => ’token*’, ‘dev’ => ’token*’, ‘prod’ => ’’ //如果测试真实请填写授权token ]; /** * 重写setUp / public function setUp() { parent::setUp(); $this->seeder = false; if (method_exists($this, ‘factory’)) { $this->app->make(‘db’); $this->factory($this->app->make(Factory::class)); if (method_exists($this, ‘seeder’)) { if (!method_exists($this, ‘seederRollback’)) { dd(“请先创建seederRollback回滚方法”); } $this->seeder = true; $this->seeder(); } } } /* * 重写tearDown / public function tearDown() { if ($this->seeder && method_exists($this, ‘seederRollback’)) { $this->seederRollback(); } parent::tearDown(); } /* * 获取地址 * @param string $apiKey * @param string $token * @return string / protected function getRequestUri($apiKey = ’list’, $token = ‘dev’, $ddinfoQuery = true) { $query = “?token=” . static::TOKEN[strtolower($token)]; if ($ddinfoQuery) { $query = $query . “&” . http_build_query(static::DDINFO); } return $apiUri = static::DOMAIN . static::API_URI[$apiKey] . $query; }}phpunit-debug-demo.xml本文件是我们单独为某些正在测试的测试用例,直接编写的xml,可以不用来回测试,已经测试成功的测试用例了,最后全部编写完测试用例,再用正式phpunit.xml即可,具体在运行测试阶段看如何指定配置<?xml version=“1.0” encoding=“UTF-8”?><phpunit bootstrap="../../bootstrap.php" convertErrorsToExceptions=“true” convertNoticesToExceptions=“false” convertWarningsToExceptions=“false” colors=“true”> <filter> <whitelist processuncoveredfilesfromwhitelist=“true”> <directory suffix=".php">../../../app/Http/Controllers/Headline</directory> <directory suffix=".php">../../../app/Http/Requests/Headline</directory> <directory suffix=".php">../../../app/Models/Headline</directory> <exclude><file>../../../app/Models/Headline/ArticleKeywordsRelationModel.php</file> </exclude> </whitelist> </filter> <testsuites> <testsuite name=“Headline Test Suite”> <directory>./</directory> </testsuite> </testsuites> <php> <ini name=“date.timezone” value=“PRC”/> <env name=“APP_ENV” value=“DEV”/> </php> <logging> <log type=“coverage-html” target=“logs/html/” lowUpperBound=“35” highLowerBound=“70”/> <log type=“json” target=“logs/logfile.json”/> <log type=“tap” target=“logs/logfile.tap”/> <log type=“junit” target=“logs/logfile.xml” logIncompleteSkipped=“false”/> <log type=“testdox-html” target=“logs/testdox.html”/> <log type=“testdox-text” target=“logs/testdox.txt”/> </logging> <listeners> <!–<listener class="\Test\Cases\Headline\Listeners\MyListener" file="./Listeners/MyListener.php">–> <!–<arguments>–> <!–<array>–> <!–<element key=“0”>–> <!–<string>Sebastian</string>–> <!–</element>–> <!–</array>–> <!–<integer>22</integer>–> <!–<string>April</string>–> <!–<double>19.78</double>–> <!–<null/>–> <!–<object class=“stdClass”/>–> <!–</arguments>–> <!–</listener>–> <!–<listener class="\Test\Cases\Headline\Listeners\MyListener" file="./Listeners/MyListener.php">–> <!–<arguments>–> <!–<array>–> <!–<element key=“0”>–> <!–<string>Sebastian</string>–> <!–</element>–> <!–</array>–> <!–<integer>22</integer>–> <!–</arguments>–> <!–</listener>–> </listeners></phpunit>测试用例案例<?php/* * Created by PhpStorm. * User: qikailin * Date: 2019-01-29 * Time: 11:57 /namespace Test\Cases\Headline\Articles;use App\Http\Controllers\Headline\ArticleController;use App\Models\Headline\ArticleCategoryRelationModel;use App\Models\Headline\ArticleContentModel;use App\Models\Headline\ArticleKeywordsRelationModel;use App\Models\Headline\ArticlesModel;use Faker\Generator;use Illuminate\Http\Request;use Test\BaseCase;class ArticleTest extends BaseCase{ private static $model; public static function setUpBeforeClass() { parent::setUpBeforeClass(); self::$model = new ArticlesModel(); } /* * 生成factory faker 数据构建模型对象 * @codeCoverageIgnore / public function factory($factory) { $words = [“测试”, “文章”, “模糊”, “搜索”]; $id = 262; $factory->define(ArticlesModel::class, function (Generator $faker) use (&$id, $words) { $id++; return [ ‘id’ => $id, ‘uri’ => $faker->lexify(‘T???????????????????’), ’title’ => $id == 263 ? “搜索” : $words[rand(0, sizeof($words) - 1)], ‘authorId’ => 1, ‘state’ => 1, ‘isUpdated’ => 0, ]; }); } /* * 生成模拟的数据,需seederRollback 成对出现 / public function seeder() { $articles = factory(ArticlesModel::class, 10)->make(); foreach ($articles as $article) { // 注意: article为引用对象,不是copy if ($article->isRecommend) { $article->recommendTime = time(); } $article->save(); } } /* * getArticleList 测试数据 * @return array / public function getArticleListDataProvider() { return [ [1, “搜索”, 1, 10, 1], [2, “搜索”, 1, 10, 0], [2, null, 1, 10, 0], [3, “搜索”, 1, 10, 0], [1, null, 1, 10, 1], [2, null, 1, 10, 0], [3, null, 1, 10, 0], ]; } /* * @dataProvider getArticleListDataProvider / public function testGetArticleList($type, $searchText, $page, $pageSize, $expceted) { $rst = self::$model->getArticleList($type, $searchText, $page, $pageSize); $this->assertGreaterThanOrEqual($expceted, sizeof($rst)); $rst = self::$model->getArticleCount($type, $searchText); $this->assertGreaterThanOrEqual($expceted, $rst); } /* * addArticle 测试数据 * @return array / public function addArticleDataProvider() { return [ [ [ ‘id’ => 273, ‘uri’ => ‘dddddddddd0123’ ], ‘save’, 0 ], [ [ ‘id’ => 274, ‘uri’ => ‘dddddddddd123’ ], ‘publish’, 0 ], [ [ ‘id’ => 275, ‘uri’ => ‘dddddddddd456’ ], ‘preview’, 0 ], ]; } /* * @dataProvider addArticleDataProvider / public function testAdd($data, $action, $expected) { $rst = self::$model->addArticle($data, $action); if ($rst) { self::$model::where(‘id’, $rst)->delete(); } $this->assertGreaterThanOrEqual($expected, $rst); } public function testGetArticleInfo() { $rst = self::$model->getArticleInfo(263, 0); $this->assertGreaterThanOrEqual(1, sizeof($rst)); $rst = self::$model->getArticleInfo(2000, 1); $this->assertEquals(0, sizeof($rst)); } /* * 回滚模拟的数据到初始状态 */ public function seederRollback() { self::$model::where(‘id’, ‘>=’, 263)->where(‘id’, ‘<=’, 272)->delete(); }}运行测试cd {APPROOT}/tests/Cases/Headline# mv phpunit-debug-custom.xml -> phpunit-debug.xml../../../vendor/bin/phpunit –verbose -c phpunit-debug.xml参考PHPUnit 5.0 官方中文手册 ...

February 15, 2019 · 3 min · jiezi

序列模型简介——RNN, Bidirectional RNN, LSTM, GRU

摘要: 序列模型大集合——RNN, Bidirectional RNN, LSTM, GRU既然我们已经有了前馈网络和CNN,为什么我们还需要序列模型呢?这些模型的问题在于,当给定一系列的数据时,它们表现的性能很差。序列数据的一个例子是音频的剪辑,其中包含一系列的人说过的话。另一个例子是英文句子,它包含一系列的单词。前馈网络和CNN采用一个固定长度作为输入,但是,当你看这些句子的时候,并非所有的句子都有相同的长度。你可以通过将所有的输入填充到一个固定的长度来解决这个问题。然而,它们的表现仍然比RNN要差,因为这些传统模型不了解给定输入的上下文环境。这就是序列模型和前馈模型的主要区别所在。对于一个句子,当看到一个词的时候,序列模型试图从在同一个句子中前面的词推导出关系。当我们读一个句子的时候,不会每次遇到一个新词都会再从头开始。我们会根据对所读过单词的理解来处理之后的每个单词。循环神经网络(Recurrent Neural Network,RNN)循环神经网络如上图所示。在一个时间步骤中的每个节点都接收来自上一个节点的输入,并且这可以用一个feedback循环来表示。我们可以深入这个feedback循环并以下图来表示。在每个时间步骤中,我们取一个输入x_i和前一个节点的输出a_i-1,对其进行计算,并生成一个输出h_i。这个输出被取出来之后再提供给下一个节点。此过程将一直继续,直到所有时间步骤都被评估完成。描述如何在每个时间步骤上计算输出的方程式,如下所示:在循环神经网络中的反向传播发生在图2中所示箭头的相反方向上。像所有其它的反向传播技术一样,我们评估一个损失函数,并获取梯度来更新权重参数。循环神经网络中有意思的部分是从右到左出现的反向传播。由于参数从最后的时间步骤更新到最初的时间步骤,这被称为通过时间的反向传播。长短期记忆(Long Short-Term Memory)— LSTM网络循环神经网络的缺点是,随着时间步骤长度的增大,它无法从差得很远的时间步骤中获得上下文环境。为了理解时间步骤t+1的上下文环境,我们有可能需要了解时间步骤0和1中的表示。但是,由于它们相差很远,因此它们所学的表示无法在时间步骤t+1上向前移动,进而对其起作用。“我在法国长大……我能说一口流利的法语”,要理解你说的法语,网络就必须远远地往后查找。但是,它不能这么做,这个问题可以归咎于梯度消失的原因。因此,循环神经网络只能记住短期存储序列。为了解决这个问题,Hochreiter & Schmidhuber提出了一种称为长短期记忆网络。LSTM网络的结构与循环神经网络保持一致,而重复模块会进行更多的操作。增强重复模块使LSTM网络能够记住长期依赖关系。让我们试着分解每个操作,来帮助网络更好地记忆。1、忘记门操作我们从当前时间步骤获取输入,并从前一时间步骤获取学习的表示,之后将它们连接起来。我们将连接后的值传递给一个sigmoid函数,该函数输出一个介于0和1之间的值(f_t)。我们在f_t和c_t-1之间做元素的乘积。如果一个值为0,那么从c_t-1中去掉,如果这个值为1,则完全通过。因此,这种操作也被称为“忘记门操作”。2、更新门操作上图表示的是“更新门操作”。我们将来自当前时间步骤中的值和前一时间步骤中已学习的表示连接起来。将连接的值通过一个tanh函数进行传递,我们生成一些候选值,并通过一个sigmoid函数传递,从候选值中选择一些值,所选的候选值将会被更新到c_t-1。3、输出门操作我们将当前时间步骤的值和前一时间步骤已学习的表示连接起来,并经由一个sigmoid函数传递来选择将要用作输出的值。我们获取单元状态并请求一个tanh函数,然后执行元素方式操作,其只允许选定的输出通过。现在,在一个单一单元中要完成很多的操作。当使用更大的网络时,与循环神经网络相比,训练时间将显著地增加。如果想要减少你的训练时间,但同时也使用一个能记住长期依赖关系的网络,那么还有另一个替代LSTM网络的方法,它被称为门控循环单元。门控循环单元(Gated Recurrent Unit ,GRU Network)与LSTM网络不同的是,门控循环单元没有单元状态,并且有2个门而不是3个(忘记、更新和输出)。门控循环单元使用一个更新门和一个重置门。更新门决定了应该让多少之前的信息通过,而重置门则决定了应该丢弃多少之前的信息。 在上面的图中,z_t表示更新门操作,通过使用一个sigmoid函数,我们决定让哪些之前的信息通过。h_t表示重置门操作,我们将前一时间步骤和当前时间步骤的连接值与r_t相乘。这将产生我们希望从前一时间步骤中所放弃的值。尽管门控循环单元在计算效率上比LSTM网络要高,但由于门的数量减少,它在表现方面仍然排在LSTM网络之后。因此,当我们需要更快地训练并且手头没有太多计算资源的情况下,还是可以选择使用门控循环单元的。双向循环神经网络所有上述双向RNN网络的一个主要问题是,它们从之前的时间步骤中学习表示。有时,你有可能需要从未来的时间步骤中学习表示,以便更好地理解上下文环境并消除歧义。通过接下来的列子,“He said, Teddy bears are on sale” and “He said, Teddy Roosevelt was a great President。在上面的两句话中,当我们看到“Teddy”和前两个词“He said”的时候,我们有可能无法理解这个句子是指President还是Teddy bears。因此,为了解决这种歧义性,我们需要往前查找。这就是双向RNN所能实现的。双向RNN中的重复模块可以是常规RNN、LSTM或是GRU。双向RNN的结构和连接如图10所示。有两种类型的连接,一种是向前的,这有助于我们从之前的表示中进行学习,另一种是向后的,这有助于我们从未来的表示中进行学习。正向传播分两步完成:我们从左向右移动,从初始时间步骤开始计算值,一直持续到到达最终时间步骤为止;我们从右向左移动,从最后一个时间步骤开始计算值,一直持续到到达最终时间步骤为止;结论将双向循环神经网络与LSTM模块相结合可以显著地提高性能,当将它们与监控机制相结合的时候,你可以在机器翻译、情感化分析等实例中获得最高水品的性能表现。希望本文对大家有帮助。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 28, 2019 · 1 min · jiezi

如何使用阿里云ARMS轻松重现用户浏览器问题

客户投诉不断,本地却无法重现?页面加载较慢是用户经常会反馈的问题,也是前端非常关注的问题之一。但定位、排查解决这类问题就通常会花费非常多的时间,主要原因如下:页面是在用户端的浏览器上加载执行,复现困难页面上线前,开发同学都会进行测试,在测试环境下页面加载一般都是正常的才会正式上线。用户在访问页面时,页面的加载是在用户端的浏览器上进行的,由于页面的加载耗时与地域、网络情况、浏览器或者运营商等有关系,想知道用户在访问页面时的具体情况,复现是非常困难的。监控信息缺少,导致无法深入排查大部分前端监控会通过PerformanceTiming对象,获取完整的页面加载耗时信息,但这类监控就缺失了页面静态资源的加载情况,无法直接复现现场,从而无法深入定位性能瓶颈。为了方便用户更快地定位性能瓶颈,阿里云ARMS前端监控推出一新功能: 会话追踪,提供页面静态资源加载的性能瀑布图,根据页面性能数据可深入定位页面资源加载情况。如何通过会话追踪帮助你快速定位问题在阿里云ARMS前端监控SDK上将sendResource配置为true,重新部署应用后,在页面onload时会上报当前页面加载的静态资源信息。从而在阿里云前端监控平台即可以对慢页面加载问题快速进行定位。SDK配置在阿里云ARMS前端监控SDK部分,默认是不上报页面加载的静态资源信息的,如果想获取页面加载的静态资源信息,只需在SDK的config部分将sendResource配置为true,重新部署后,就可以上报相关信息。具体配置如下:<script>!(function(c,b,d,a){c[a]||(c[a]={});c[a].config={pid:“atc889zkcf@8cc3f63543da641”,imgUrl:“https://arms-retcode.aliyuncs.com/r.png?",sendResource:true};with(b)with(body)with(insertBefore(createElement("script"),firstChild))setAttribute("crossorigin","",src=d)})(window,document,"https://retcode.alicdn.com/retcode/bl.js","__bl");</script>注意:静态资源加载信息的上报是在页面onload时会触发,上报信息量较大,如果对于页面性能要求很高的应用,可以不开启该配置。问题排查过程1. 发现问题进入访问速度菜单后,发现页面的性能较差,11点钟的页面完全加载时间达到35s,如下:2. 慢页面会话追踪在慢页面会话追踪模块,提供该页面在指定时间段内加载较慢的TOP20,这样可以快速发现哪些会话加载较慢,如下图所示。在该模块,你可以快速发现在11点钟有一次会话的页面加载时间在36.72s,这次访问应该是直接导致页面加载时间详情中折线图突然暴增的原因了。其中在在模块有7次会话访问的页面加载时间在7s以上,点击对应的页面,可以直接进入到会话详情页面,从而直观查看页面静态资源加载的瀑布图。通过页面资源加载的瀑布图,可以快速定位到资源加载的性能瓶颈,同时可以查看本次访问的客户端IP地址、浏览器、操作系统等UA信息,从而进一步确认是由于网络原因还是其他原因导致的,针对性进行相应的优化。3. 其他发现问题入口会话追踪也可以进入“会话追踪”菜单,可以看到该应用下的会话列表。会话列表中会根据页面完全加载时间排序,展示TOP100,帮助用户可以快速发现耗时较长的会话信息。同时支持按照页面、会话Id、浏览器、浏览器版本号进行过滤,展示相关的会话信息。点击操作后,是该会话的页面资源加载详情。访问明细如果当前会话列表中无法找到你要排查的会话信息,可以通过访问明细查找到相应的日志详细信息,在param中找到对应的sid即会话Id,然后在会话列表中查找相应的会话Id,即可以定位到想排查的会话信息。例如:在已知用户的客户端IP的情况下,想定位相应的会话信息,即可以在访问明细中,通过t=res and 117.136.32.110 进行搜索,找到对应的会话Id。根据查找到的会话Id, 就可以在会话列表中进行过滤,定位到具体的会话内容。使用入口指南进入访问速度菜单,如果发现页面性能较差,可以在"慢页面会话追踪Top20"中查看访问较慢的会话情况点击详情后,可以查看具体的页面资源加载瀑布图如果Top20不满足,可以点击"更多”,从而进入"会话列表"进入会话追踪菜单,展示的是TOP100的会话列表信息,根据页面完全加载时间从高到底排序,排查页面资源加载情况至此,慢页面会话追踪功能及使用方法介绍完成。该功能可以帮助你复现用户在访问页面时的页面资源加载情况,快速定位性能瓶颈问题。附录官网文档介绍阿里云ARMS前端监控官网本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 23, 2019 · 1 min · jiezi

从NeurIPS 2018看AI发展路线!

摘要: 从NeurIPS 2018看AI发展路线!去年9月份的时候,我发表过一份技术报告,阐述了我认为人工智能最重要的挑战,大概有以下四个方面:可伸缩性(Scalability)**计算或存储的成本不与神经元的数量成二次方或线性比例的神经网络;持续学习(Continual Learning)**那些必须不断地从环境中学习而不忘记之前获得的技能和重置环境能力的代理;元学习(Meta-Learning)**为了改变自己的学习算法而进行自我参照的代理;基准(Benchmarks)**具有足够复杂的结构和多样性的环境,这样智能代理就可以派上用场了,而无需对强感应偏差进行硬编码;在2018年NeurIPS会议期间,我调查了目前其他研究人员关于这些问题的方法和观点,以下是报告的具体内容:可伸缩性很明显,如果我们用人工神经网络来实现人类大脑中所发现的1000亿个神经元,标准的二维矩阵乘积并没有多大的用处。模块层由一个模块池和一个控制器组成,控制器根据输入来选择要执行的模块为了解决这个问题,我在2018年的NeurIPS上发表了研究性论文《模块化网络:学习分解神经计算》。不评估对于每个输入元素的整个ANN,而是将网络分解为一组模块,其中只使用一个子集,这要取决于输入。这个过程是受人脑结构的启发,在其中我们使用了模块化,这也是为了改善对环境变化的适应能力和减轻灾难性的遗忘。在这个方法中,我们学习到了这些模块的参数,以及决定哪些模块要一起使用。以往有关条件计算的文献都记载着许多模块崩溃的问题,即优化过程忽略了大部分可用的模块,从而导致没有用最优的解决办法。我们基于期望最大化的方法可以防止这类问题的发生。遗憾的是,强行将这种分离划分到模块有其自身的问题,我们在《模块化网络:学习分解神经计算》中相继讨论了这些问题。相反地,我们可能会像我在关于稀疏性的技术报告中讨论的那样,设法在权重和激活中利用稀疏性和局部性。简而言之,我们只想对少数非零的激活执行操作,丢弃权重矩阵中的整行。此外,如果连通性是高度稀疏的,那么我们实际上可以将二次方成本降到一个很小的常数。这种的条件计算和未合并的权值访问在当前的GPU上实现的成本非常的高,通常来说不太值得操作。Nvidia处理条件计算和稀疏性NVIDIA一个软件工程师说,目前还没有计划建造能够以激活稀疏性的形式而利用条件计算的硬件。主要原因似乎是通用性与计算速度之间的权衡。为这个用例搭建专用硬件所花费的成本太高了,因为它有可能会限制其它机器学习的应用。相反,NVIDIA目前从软件的角度更加关注权重的稀疏性。GraphCore处理的条件计算和稀疏性GraphCore搭建的硬件允许在靠近处理单元的缓存中向前迁移期间存储激活,而不是在GPU上的全局存储内存中。它还可以利用稀疏性和特定的图形结构,在设备上编译并建立一个计算图形。遗憾的是,由于编译成本太高,这个结构是固定的,不允许条件计算。作为一个整体的判断,对于范围内的条件计算似乎没有对应的硬件解决方案,目前来说我们在很大程度上必须坚持多机器并行的方式。在这方面,NeurIPS发布了一种全新的分配梯度计算方法—Mesh-Tensorflow,该方法不仅可以横跨多机进行计算,还可以跨模型计算,甚至允许更大的模型以分布式的方式进行训练。持续学习长期以来,我一直主张基于深度学习的持续学习系统,即它们能够不断地从经验中学习并积累知识,当新任务出现的时候,这些系统可以提供之前积累的知识以帮助学习。本身,它们需要能够向前迁移,以及防止灾难性的遗忘。NeurIPS的持续学习研讨会正是讨论这些问题的。虽然这两个标准也许是不完整的,但是多个研究者(Mark Ring,Raia Hadsell)提出了一个更大的列表:向前迁移向后迁移无灾难性的遗忘无灾难性的冲突可扩展(固定的存储和计算)可以处理未标记的任务边界能够处理偏移无片段无人控制无可重复状态在我看来,解决这个问题的方法有六种:(部分)重放缓冲区重新生成以前经验的生成模型减缓重要权重的训练冻结权重冗余(更大的网络->可伸缩性)条件计算(->可扩展性)以上这些方法的任何一个都不能处理上述持续学习列表里的所有问题。遗憾的是,这在实践中也是不可能的。在迁移和内存或计算之间总是有一个权衡,在灾难性遗忘和迁移或者内存或者计算之间也总是有一个权衡。因此,很难完全地、定量地衡量一个代理的成功与否。相反,我们应该建立基准环境,要求持续学习代理具备我们所需要的能力,例如,在研讨会上展示的基于星际争霸(Starcraft)的环境。此外,Raia Hadsell认为,持续学习涉及到从依赖i.i.d.(Independent and Identically distributed)数据的学习算法转向从非平稳性分布中学习。尤其是,人类擅长逐步地学习而不是IID。因此,当远离IID需求时,我们有可能能够解锁一个更强大的机器学习范式。论文《通过最大限度地迁移和最小化干扰的持续学习(Continual Learning by Maximizing Transfer and Minimizing Interference)》表明REPTILE(MAML继承者)和减少灾难性遗忘之间有着一个有趣的联系。从重放缓冲区中提取的数据点的梯度(显示在REPTILE)之间的点积导致梯度更新,从而最小化干扰并减少灾难性遗忘。讨论小组内有人认为,我们应该在控制设置环境中进行终身学习实验,而不是监督学习和无监督学习,以防止算法的开发与实际应用领域之间的任何不匹配。折现系数虽然对基于贝尔曼方程(Bellman Equation)的学习是有帮助的,但对于更现实的增强学习环境设置来说可能存在问题。此外,任何学习,特别是元学习,都会由于学分分配而受到固有的限制。因此,开发具有低成本学分分配的算法是智能代理的关键。元学习元学习就是关于改变学习算法其本身。这可能是改变一个内部优化循环的外部优化循环,一个可以改变自身的自引用算法。许多研究人员还关注着快速适应性,即正向迁移,到新的任务或者环境等等。如果我们将一个学习算法的初始参数看作它自己的一部分,则可以将其视为迁移学习或者元学习。Chelsea Finn的一个最新算法—MAML(未知模型元学习法),他对这种快速适应性算法产生了极大的兴趣。例如,MAML可以用于基于模型的强化学习,其中的模型可以快速地进行动态改变。在进化策略梯度(Evolved Policy Gradients ,EPG)中,损失函数使用随机梯度下降法优化策略的参数,同时损失函数的参数也改进了。一个有趣的想法是代理轨迹和策略输出的可区分损失函数的学习。这允许在使用SGD来训练策略时,对损失函数的几个参数进行改进。与此同时,进化策略梯度的作者们表明了,学习到的损失函数通过回报函数进行了泛化,并允许有快速适应性。它的一个主要问题是学分分配非常缓慢:代理必须使用损失函数进行完全地训练,以获得元学习者的平均回报(适合度)。对于学习过的优化器的损失情况变得更加难以控制我在元学习研讨会上的另一个有趣发现是元学习者损失情况的结构。Luke Metz在一篇关于学习优化器的论文中指出,随着更新步骤的展现,优化器参数的损失函数变得更加复杂。我怀疑这是元学习算法的普遍行为,参数值的微小改变可以关系到最终表现中的巨大变化。我对这种分析非常感兴趣。在学习优化的案例中,Luke通过变分优化(Variational Optimization)—进化策略的一种原则性解释,以此缓和损失情况来解决这个问题。基准目前大多数强化学习算法都是以游戏或模拟器为基准环境的,比如ATARI 或者是Mujoco。这些是简单的环境,与现实世界中的复杂性几乎没什么相似之处。研究人员经常唠叨的一个主要问题是,我们的算法来自低效的样本。通过非策略优化和基于模型的强化学习,可以更有效地利用现有数据,从而部分解决这一问题。然而,一个很大的因素是我们的算法没有之前在这些基准中使用过的经验。我们可以通过在算法中手工归纳偏差来避开这一问题,这些算法反映了某些先验知识,但是搭建允许在未来可以利用知识积累的环境有可能更有趣。据我所知,直到现在还没有这种基准环境。雷艇(Minecraft)模拟器可能是最接近这些要求的了。持续学习星际争霸(Starcraft)环境是一个以非常简单的任务开始的课程。对于如此丰富的环境,另外一种选择是建立明确的课程,如前面提到的星际争霸环境,它是由任务课程组成的。这在一定程度上也是Shagun Sodhani在他的论文《Environments for Lifelong Reinforcement Learning》。他在清单上列出了:环境多样性随机性自然性非平稳性多形式短期和长期目标多代理因果相互影响游戏引擎开发商Unity3D发布了一个ML-Agents工具包,用于在使用Unity的环境搭建中进行训练和评估代理。一般来说,现实环境搭建的一个主要问题是需求与游戏实际设计有本质的不同:为了防止过拟合,重要的是,在一个广阔的世界里,物体看起来都是不一样的,因此不能像在电脑游戏中经常做的那样被复制。这意味着为了真正的泛化,我们需要生成的或精心设计的环境。最后,我相信可以使用计算来生成非平稳环境,而不是通过手动来搭建。例如,这有可能是一个具有与现实世界类似环境的物理模拟器。为了节省计算资源,我们也可以从基于三维像素的简化工作开始。如果这个模拟过程呈现了正确的特性,我们有可能可以模拟一个类似于进化的过程,来引导一个非平稳的环境,开发出许多相互影响的生命形式。这个想法很好地拟合了模拟假设(simulation hypothesis)理论,并且与Conway’s Game of Life有一定的联系。这种方法的主要问题是产生的复杂性与人类已知的概念没有相似点。与此同时,由此产生的智能代理将无法迁移到现实世界中。最近,我发现Stanley和Clune的团队在他们的论文《假想:不断地生成越来越复杂和多样化的学习环境(POET: Endlessly Generating Increasingly Complex and Diverse Learning Environments)》中已经部分地实现了这个想法。环境是非平稳性的,可以被看作是一个用于最大化复杂性和代理学习进程的代理。他们将这一观点称为开放式学习,我建议你阅读一下这篇文章。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 18, 2019 · 1 min · jiezi

2018最佳GAN论文回顾(下)

摘要: 继上一篇《2018最佳GAN论文回顾(上)》,我又继续介绍了一个对于GAN的基于样式的生成器体系结构的新论文,提出了一个新的模型来应对这种挑战。继上一篇《2018最佳GAN论文回顾(上)》,我又继续介绍了一个对于GAN的基于样式的生成器体系结构的新论文,提出了一个新的模型来应对这种挑战。一种用于生成式对抗网络的基于生成器体系结构的方式 (A Style-Based Generator Architecture for Generative Adversarial Networks)这是NVIDIA的一篇新论文,一个对于GAN(StyleGAN)的基于样式的生成器体系结构,提出了一个新的模型来应对这个挑战。StyleGAN是一步一步地生成人工图像的,从非常低的分辨率开始,一直到高分辨率(1024×1024)。通过分别地修改网络中每个级别的输入,它可以控制在该级别中所表示的视觉特征,从粗糙的特征(姿势、面部形状)到精细的细节(头发颜色),而不会影响其它的级别。这种技术不仅可以更好地理解所生成的输出,而且还可以产生最高水平的结果 — 比以前生成的图像看起来更加真实的高分辨率图像。2018年NVIDIA首次使用ProGAN应对这一挑战时,研究人员都无法生成高质量的大图像(如:1024×1024)。ProGAN的关键创新点是渐进式训练 — 它首先使用非常低分辨率的图像(如:4×4)开始训练生成器和识别器,并且每次都增加一个更高分辨率的网络层。这项技术首先通过学习即使在低分辨率图像中也可以显示的基本特征,来创建图像的基本部分,并且随着分辨率的提高和时间的推移,学习越来越多的细节。低分辨率图像的训练不仅简单、快速,而且有助于更高级别的训练,因此,整体的训练也就更快。ProGAN生成高质量的图像,但与大多数模型一样,它控制所生成图像的特定特征的能力非常有限。换句话说,这些特性是互相关联的,因此尝试调整一下输入,即使是一点儿,通常也会同时影响多个特性。一个很好的类比就是基因组,在其中改变一个基因可能影响多个特性。StyleGAN如何工作StyleGAN论文提供了一个升级版本的ProGAN图像生成器,重点关注生成器网络。作者们观察到ProGAN渐进层的一个潜在的好处是,如果使用得当,它们能够控制图像的不同视觉特征。层和分辨率越低,它所影响的特征就越粗糙。本文将这些特征分为三种类型:1、粗糙的—分辨率最高82,影响姿势、一般发型、面部形状等;2、中等的—分辨率为162至322,影响更精细的面部特征、发型、眼睛的睁开或是闭合等;3、高质的—分辨率为642到10242,影响颜色方案(眼睛、头发和皮肤)和微观特征;除ProGAN生成器之外的一些:映射网络映射网络的目标是将输入向量编码为中间向量,中间向量的不同元素控制不同的视觉特征。这是一个非常重要的过程,因为使用输入向量来控制视觉特征的能力是非常有限的,因为它必须遵循训练数据的概率密度。例如,如果黑头发的人的图像在数据集中更常见,那么更多的输入值将会被映射到该特征上。因此,该模型无法将部分输入(向量中的元素)映射到特征上,这一现象被称为特征纠缠。然而,通过使用另一个神经网络,该模型可以生成一个不必遵循训练数据分布的向量,并且可以减少特征之间的相关性。映射网络由8个全连接的层组成,它的输出ⱳ与输入层(512×1)的大小相同。样式模块(AdaIN)AdaIN(自适应实例标准化)模块将映射网络创建的编码信息ⱳ传输到生成的图像中。该模块被添加到合成网络的每个分辨率级别中,并定义该级别中特征的可视化表达式:1、卷积层输出的每个通道首先进行标准化,以确保步骤3的缩放和切换具有预期的效果;2、中间向量ⱳ使用另一个全连接的网络层(标记为A)转换为每个通道的比例和偏差;3、比例和偏差的向量切换卷积输出的每个通道,从而定义卷积中每个过滤器的重要性。这个调优操作将信息从ⱳ转换为可视的表达方式;删除传统输入大多数的模型以及其中的ProGAN使用随机输入来创建生成器的初始图像(即4×4级别的输入)。StyleGAN团队发现图像特征是由ⱳ和AdaIN控制的,因此可以忽略初始输入,并用常量值替代。虽然本文没有解释它为什么能提高性能,但一个保险的假设是它减少了特征纠缠,对于网络在只使用ⱳ而不依赖于纠缠输入向量的情况下更容易学习。随机变化人们的脸上有许多小的特征,可以看作是随机的,例如:雀斑、发髻线的准确位置、皱纹、使图像更逼真的特征以及各种增加输出的变化。将这些小特征插入GAN图像的常用方法是在输入向量中添加随机噪声。然而,在许多情况下,由于上述特征的纠缠现象,控制噪声的影响是很复杂的,从而会导致图像的其它特征受到影响。StyleGAN中的噪声以类似于AdaIN机制的方式添加,在AdaIN模块之前向每个通道添加一个缩放过的噪声,并稍微改变其操作的分辨率级别特征的视觉表达方式。样式混合StyleGAN生成器在合成网络的每个级别中使用了中间向量,这有可能导致网络学习到这些级别是相关的。为了降低相关性,模型随机选择两个输入向量,并为它们生成了中间向量ⱳ。然后,它用第一个输入向量来训练一些网络级别,然后(在一个随机点中)切换到另一个输入向量来训练其余的级别。随机的切换确保了网络不会学习并依赖于一个合成网络级别之间的相关性。虽然它并不会提高所有数据集上的模型性能,但是这个概念有一个非常有趣的副作用 — 它能够以一种连贯的方式来组合多个图像(视频请查看原文)。该模型生成了两个图像A和B,然后通过从A中提取低级别的特征并从B中提取其余特征再组合这两个图像。在W中的截取技巧在生成模型中的一个挑战,是处理在训练数据中表现不佳的地方。这导致了生成器无法学习和创建与它们类似的图像(相反,它会创建效果不好的图像)。为了避免生成较差的图像,StyleGAN截断了中间向量ⱳ,迫使它保持接近“平均”的中间向量。对模型进行训练之后,通过选择多个随机的输入,用映射网络生成它们的中间向量,并计算这些向量的平均值,从而生成“平均”的平均值ⱳ。当生成新的图像时,不用直接使用映射网络的输出,而是将值ⱳ转换为ⱳ_new=ⱳ_avg+�(ⱳ -ⱳ_avg),其中�的值定义了图像与“平均”图像的差异量(以及输出的多样性)。有趣的是,在仿射转换块之前,通过对每个级别使用不同的�,模型可以控制每个特征集与平均值的差异量。微调在ProGAN上,StyleGAN的另外一个改进措施是更新几个网络超参数,例如训练持续时间和损失函数,并将离得最近的放大或缩小尺度替换为双线性采样。结果本文介绍了两个数据集的最新结果,一个是由名人图片组成的— CelebA-HQ,另一个是由“普通”人图片组成的、更加多样化的新数据集— Flickr-Faces-HQ (FFHQ)。下图显示了模型的不同配置的Frèchet inception distance (FID)得分与ProGAN相比,模型在不同配置下的性能(FID得分),分数越低模型越好(来源:StyleGAN)除了这些结果之外,本文还说明了该模型并不仅仅是通过在卧室图像和汽车图像两个数据集上展示其结果而定制的。特征分离为了使关于特征分离的讨论更加的量化,本文提出了两种新的特征分离的测量方法:1、感知路径长度 — 当在两个随机输入之间插入时,测量两个连续图像(它们的VGG16嵌入)之间的差异。剧烈的变化意味着多个特性已经同时改变了,它们有可能会被纠缠;2、线性可分离性 — 是将输入按照二进制类进行分类的能力,如男性和女性。分类越好,特征就越容易区分。通过对输入的向量z和中间向量ⱳ的指标进行比较,作者们发现在ⱳ中的特征很明显地更容易分离。这些指标还表明了在映射网络中选择8个层与选择1到2个层相比的好处。实施细节StyleGAN在CelebA-HQ和FFHQ数据集上接受了为期一周的训练,使用了8个Tesla V100 GPU。它是在TensorFlow中实现的,并且将开源的。结论StyleGAN是一篇突破性的论文,它不仅可以生成高质量的和逼真的图像,而且还可以对生成的图像进行较好的控制和理解,甚至使生成可信度较高的假图像变得比以前更加的容易。在StyleGAN中提出的一些技术,特别是映射网络和自适应实例标准化(AdaIN),可能是未来许多在GAN方面创新的基础。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 16, 2019 · 1 min · jiezi

重磅公开!阿里语音识别模型端核心技术,让你“听”见未来

阿里妹导读:语音识别技术作为人工智能技术中的重要组成部分,成为影响人机交互的核心组件之一,从各种智能家用IoT设备的语音交互能力,到公共服务、智慧政务等场合的应用,语音识别技术正在影响着人们生活的方方面面。本文将全面介绍阿里云语音识别技术中的重要模型端技术,希望和业界同仁交流探讨。声学模型、语言模型和解码器可以看作是现代语音识别系统最核心的三个组成部分。虽然最近有一些研究者尝试构建End2end的语音识别系统,但包含声学模型、语言模型和解码器的现代语音识别系统依然是当前最主流和使用最广泛的系统。在这其中,声学模型主要用来构建输入语音和输出声学单元之间的概率映射关系;语言模型用来描述不同字词之间的概率搭配关系,使得识别出的句子更像自然文本;解码器负责结合声学单元概率数值和语言模型在不同搭配上的打分进行筛选,最终得到最可能的识别结果。随着近几年深度学习的火热,语音识别领域也纷纷投入深度学习的大潮之中。将传统HMM-GMM声学模型替换成HMM-DNN声学模型后,可以获得超过20%的相对提升,在传统N-Gram语言模型基础上叠加NN-LM语言模型也可以获得进一步的提高。在这过程中,声学模型由于更适合采用深度神经网络模型,从而受到研究者更多的关注。本文主要介绍阿里云语音识别技术中采用的声学模型技术和语言模型技术,包括LC-BLSTM声学模型、LFR-DFSMN声学模型和NN-LM语言模型,其中LC-BLSTM是对传统BLSTM模型的一种改进,在保持了高准确率的同时,提供了低延时的特性;而DFSMN是一种新颖的非递归结构的神经网络却可以像RNN一样对信号的长时相关进行建模,同时可以获得更稳定的训练效果和更好的识别准确。NN-LM语言模型是近年来在传统N-Gram语言模型基础上获得的进一步改进。Latency-Controlled BLSTM模型DNN(即fully connected DNN)模型的优点在于通过增加神经网络的层数和节点数,扩展了网络对于复杂数据的抽象和建模能力,但同时DNN模型也存在一些不足,例如DNN中一般采用拼帧来考虑上下文相关信息对于当前语音帧的影响,这并不是反映语音序列之间相关性的最佳方法。自回归神经网络(RNN)在一定程度上解决了这个问题,它通过网络节点的自连接达到利用序列数据间相关性的目的。进一步有研究人员提出一种长短时记忆网络(LSTM-RNN),它可以有效减轻简单RNN容易出现的梯度爆炸和梯度消散问题,而后研究人员又对LSTM进行了扩展,使用双向长短时记忆网络(BLSTM-RNN)进行声学模型建模,以充分考虑上下文信息的影响。BLSTM模型可以有效地提升语音识别的准确率,相比于DNN模型,相对性能提升可以达到15%-20%。但同时BLSTM模型也存在两个非常重要的问题:1、句子级进行更新,模型的收敛速度通常较慢,并且由于存在大量的逐帧计算,无法有效发挥GPU等并行计算工具的计算能力,训练会非常耗时;2、由于需要用到整句递归计算每一帧的后验概率,解码延迟和实时率无法得到有效保证,很难应用于实际服务。对于这两个问题,学术界首先提出Context-Sensitive-Chunk BLSTM(CSC-BLSTM)的方法加以解决,而此后又提出了Latency Controlled BLSTM(LC-BLSTM)这一改进版本,更好、更高效地减轻了这两个问题。我们在此基础上采用LC-BLSTM-DNN混合结构配合多机多卡、16bit量化等训练和优化方法进行声学模型建模,取得了相比于DNN模型约17-24%的相对识别错误率下降。典型的LSTM节点结构由3个gate组成:input gate、forget gate、output gate和一个cell组成,输入、输出节点以及cell同各个门之间都存在连接;inputgate、forget gate同cell之间也存在连接,cell内部还有自连接。这样通过控制不同门的状态,可以实现更好的长短时信息保存和误差传播。LSTM可以像DNN一样逐层堆积成为DeepLSTM,为了更好地利用上下文信息,还可以使用BLSTM逐层堆积构造Deep BLSTM,其结构如下图所示,网络中沿时间轴存在正向和反向两个信息传递过程,每一个时间帧的计算都依赖于前面所有时间帧和后面所有时间帧的计算结果,对于语音信号这种时序序列,该模型充分考虑了上下文对于当前语音帧的影响,能够极大提高音素状态的分类准确率。然而由于标准的BLSTM是对整句语音数据进行建模,训练和解码过程存在收敛慢、延迟高、实时率低等问题,针对这些弊端我们采用了Latency Controlled BLSTM进行解决,与标准的BLSTM使用整句语音进行训练和解码不同,Latency Control BLSTM使用类似truncated BPTT的更新方式,并在cell中间状态处理和数据使用上有着自己的特点,如下图所示,训练时每次使用一小段数据进行更新,数据由中心chunk和右向附加chunk构成,其中右向附加chunk只用于cell中间状态的计算,误差只在中心chunk上进行传播。时间轴上正向移动的网络,前一个数据段在中心chunk结束时的cell中间状态被用于下一个数据段的初始状态,时间轴上反向移动的网络,每一个数据段开始时都将cell中间状态置为0。该方法可以很大程度上加快网络的收敛速度,并有助于得到更好的性能。解码阶段的数据处理与训练时基本相同,不同之处在于中心chunk和右向附加chunk的维度可以根据需求进行调节,并不必须与训练采用相同配置。LFR-DFSMN模型FSMN是近期被提出的一种网络结构,通过在前馈全连接神经网络(Feedforward Fully-connectedNeural Networks,FNN)的隐层添加一些可学习的记忆模块,从而可以有效地对信号的长时相关性进行建模。FSMN相比于LCBLSTM不仅可以更加方便的控制时延,而且往往也能获得更好的性能,需要的计算资源也更少。但是标准的FSMN很难训练非常深层的结构,由于梯度消失问题导致训练效果不好。而深层结构的模型目前在很多领域被证明具有更强的建模能力。因而针对此我们提出了一种改进的FSMN模型,称之为深层的FSMN(Deep FSMN, DFSMN)。进一步的我们结合低帧率(Low Frame Rate,LFR)技术构建了一种高效的实时语音识别声学模型,相比于去年我们上线的LFR-LCBLSTM声学模型可以获得超过20%的相对性能提升,同时可以获得2-3倍的训练以及解码的加速,可以显著的减少我们的系统实际应用时所需要的计算资源。最早提出的FSMN的模型结构如上图(a)所示,其本质上是一个前馈全连接神经网络,通过在网络的某些隐层旁添加一些记忆模块(memory block)来对当前时刻周边的上下文信息进行建模,从而使得模型可以对时序信号的长时相关性进行建模。记忆模块采用如上图(b)所示的抽头延迟结构将当前时刻以及之前 N 个时刻的隐层输出通过一组系数编码得到一个固定的表达。FSMN的提出是受到数字信号处理中滤波器设计理论的启发:任何无限响应冲击(Infinite Impulse Response, IIR)滤波器可以采用高阶的有限冲击响应(FiniteImpulseResponse, FIR)滤波器进行近似。从滤波器的角度出发,如上图(c)所示的RNN模型的循环层就可以看作如上图(d)的一阶IIR滤波器。而FSMN采用的采用如上图(b)所示的记忆模块可以看作是一个高阶的FIR滤波器。从而FSMN也可以像RNN一样有效的对信号的长时相关性进行建模,同时由于FIR滤波器相比于IIR滤波器更加稳定,因而FSMN相比于RNN训练上会更加简单和稳定。根据记忆模块编码系数的选择,可以分为:标量FSMN(sFSMN)矢量FSMN(vFSMN)sFSMN 和 vFSMN 顾名思义就是分别使用标量和矢量作为记忆模块的编码系数。以上的FSMN只考虑了历史信息对当前时刻的影响,我们可以称之为单向的FSMN。当我们同时考虑历史信息以及未来信息对当前时刻的影响时,我们可以将单向的FSMN进行扩展得到双向的FSMN。FSMN相比于FNN,需要将记忆模块的输出作为下一个隐层的额外输入,这样就会引入额外的模型参数。隐层包含的节点越多,则引入的参数越多。研究结合矩阵低秩分解(Low-rank matrix factorization)的思路,提出了一种改进的FSMN结构,称之为简洁的FSMN(Compact FSMN,cFSMN)。下图是一个第l个隐层包含记忆模块的cFSMN的结构框图。对于cFSMN,通过在网络的隐层后添加一个低维度的线性投影层,并且将记忆模块添加在这些线性投影层上。进一步的,cFSMN对记忆模块的编码公式进行了一些改变,通过将当前时刻的输出显式的添加到记忆模块的表达中,从而只需要将记忆模块的表达作为下一层的输入。这样可以有效的减少模型的参数量,加快网络的训练。上图是我们进一步提出的Deep-FSMN(DFSMN)的网络结构框图,其中左边第一个方框代表输入层,右边最后一个方框代表输出层。我们通过在cFSMN的记忆模块(红色框框表示)之间添加跳转连接(skip connection),从而使得低层记忆模块的输出会被直接累加到高层记忆模块里。这样在训练过程中,高层记忆模块的梯度会直接赋值给低层的记忆模块,从而可以克服由于网络的深度造成的梯度消失问题,使得可以稳定的训练深层的网络。相比于之前的cFSMN,DFSMN优势在于,通过跳转连接可以训练很深的网络。对于原来的cFSMN,由于每个隐层已经通过矩阵的低秩分解拆分成了两层的结构,这样对于一个包含4层cFSMN层以及两个DNN层的网络,总共包含的层数将达到13层,从而采用更多的cFSMN层,会使得层数更多而使得训练出现梯度消失问题,导致训练的不稳定性。我们提出的DFSMN通过跳转连接避免了深层网络的梯度消失问题,使得训练深层的网络变得稳定。需要说明的是,这里的跳转连接不仅可以加到相邻层之间,也可以加到不相邻层之间。跳转连接本身可以是线性变换,也可以是非线性变换。具体的实验我们可以实现训练包含数十层的DFSMN网络,并且相比于cFSMN可以获得显著的性能提升。从最初的FSMN到cFSMN不仅可以有效的减少模型的参数,而且可以获得更好的性能。进一步的在cFSMN的基础上,我们提出的DFSMN,可以更加显著的提升模型的性能。如下表是在一个2000小时的英文任务上基于BLSTM,cFSMN,DFSMN的声学模型性能对比。从上表中可以看到,在2000小时这样的任务上,DFSMN模型可以获得比BLSTM声学模型相对14%的错误率降低,显著提高了声学模型的性能。传统的声学模型,输入的是每帧语音信号提取的声学特征,每帧语音的时长通常为10ms,对于每个输入的语音帧信号会有相对应的一个输出目标。最近有研究提出一种低帧率(Low Frame Rate,LFR)建模方案:通过将相邻时刻的语音帧进行绑定作为输入,去预测这些语音帧的目标输出得到的一个平均输出目标。具体实验中可以实现三帧(或更多帧)拼接而不损失模型的性能。从而可以将输入和输出减少到原来的三分之一甚至更多,可以极大的提升语音识别系统服务时声学得分的计算以及解码的效率。我们结合LFR和以上提出的DFSMN,构建了基于LFR-DFSMN的语音识别声学模型,经过多组实验我们最终确定了采用一个包含10层cFSMN层+2层DNN的DFSMN作为声学模型,输入输出则采用LFR,将帧率降低到原来的三分之一。识别结果和去年我们上线的最好的LCBLSTM基线比较如下表所示。通过结合LFR技术,我们可以获得三倍的识别加速。从上表中可以看到,在实际工业规模应用上,LFR-DFSMN模型比LFR-LCBLSTM模型可以获得20%的错误率下降,展示了对大规模数据更好的建模特性。NN-LM语言模型语言模型,顾名思义,对语言进行建模的模型。语言表达可以看作一串字符序列,不同的字符序列组合代表不同的含义,字符的单位可以是字或者词。语言模型的任务,可以看作是给定字符序列,如何估计该序列的概率,或者说,如何估计该序列的合理性。P(上海 的 工人 师傅 有 力量)>P(上海 的 工人 食腐 有 力量)拿这句话做个例子。比如到底应该是“工人师傅有力量”,还是“工人食腐有力量”,哪句话更“合适”。我们容易判断左边这句的概率大一点。于是我们希望通过语言模型的建模,可以给出符合人类预期的概率分配。就像这句,“工人师傅”的概率,大于“工人食腐”的概率。基于统计词频的传统N元文法模型,通过马尔可夫假设简化了模型结构和计算,通过计数的方式计算,通过查找的方式使用。拥有估计简单、性能稳定、计算快捷的优势,有超过三十年的使用历史。然而其马尔科夫假设强制截断建模长度,使得模型无法对较长的历史建模;基于词频的估计方式也使得模型不够平滑,对于低词频词汇估计不足。随着神经网络(Neural Networks,NNs)的第三次崛起,人们开始尝试通过NN来进行语言模型建模。一个典型的建模结构是递归神经网络(recurrentneural networks,RNNs),其递归的结构理论上可以对无穷长序列进行建模,弥补了N元文法对于序列长度建模的不足;同时其各层间的全向连接也保证了建模的平滑。此外为了提升模型的性能,研究者们还尝试了通过长短时记忆(Long Short-Term Memory,LSTM)结构来提升基本RNN本身建模能力的不足,进一步提升模型性能。NN用于大规模语言建模的系统中,需要面对一些问题,例如大词表带来的存储和计算增加。实际线上系统的词表往往比较大,而随着词表的增加,基本RNN结构的存储和计算量都会几何级数爆炸式增长。为此,研究者们进行了一些尝试,压缩词典尺寸成了一个最直接的解决方案,一个经典的方法是词表聚类。该方法可以大幅压缩词表尺寸,但往往也会带来一定的性能衰减。更直接的一个想法是直接过滤掉低频词汇,这样依然会带来一定的性能衰减,据此有一个改进策略,我们发现真正制约速度性能的主要是输出层节点,输入层节点大,借助projection层可以很好解决,于是输入层采用大辞典,而仅对输出层词表进行抑制,这样不仅尽可能地降低了损失,同时过滤掉过低的词频,也有利于模型节点的充分训练,性能往往还会略有提升。词表的压缩可以提升建模性能,降低计算量和存储量,但仅限于一定的量级,不可以无限制压缩,如何继续降低计算量依然是一个问题。一些方法被提了出来。例如LightRNN,通过类似聚类的方式,利用embedding的思想,把词表映射到一个实值矩阵上,实际输出只需要矩阵的行加矩阵的列,计算量大概也能开个方。和节点数多一起造成计算量大的一个原因就是softmax输出,需要计算所有的节点求个和,然后得到分母。若是这个分母能保持一个常数,实际计算的时候就只算需要的节点,在测试环节就快的多了。于是就有了正则项相关的方法,Variance Regularization,如果训练速度可以接受的话,这种方法在基本不损失模型正确性的情况下可以大幅提升前向计算速度;如果训练的时候也想提速,还可以考虑基于采样,sampling的方法,比如NCE、Importance Sampling、Black Sampling等,本质上就是说,在训练的时候不计算全部节点,只计算正样本(也就是标签为1的节点),以及部分通过某种分布采样的到的负样本,避免高输出造成的计算缓慢。速度上提升还是很明显的。从阿里云获得开发者模型定制能力想象一个做智能电话客服或是智能会议系统的开发者,需要为他的系统接入语音识别(将语音转写为文字)的能力。摆在他面前的会是这样一个尴尬的局面:一个选择是自己从零开始学做语音识别,这可能要花费大量的时间和金钱。毕竟人工智能这种事情,各大互联网巨头投入大量的人力、物力、财力,也要花较长的时间才能积累下技术;第二个选择是用上述巨头们在互联网上提供的开箱即用的、one size fits all的语音识别接口,时间是省下了,但语音转文字的准确率嘛,只能碰碰运气,毕竟巨头们也很忙,没有精力为你关注的场景进行优化。那么问题来了:有没有一种手段能够以最小的投入获得业务上最佳的语音识别效果呢?答案是肯定的。阿里云依托达摩院业界领先的语音交互智能,打破传统语音技术提供商的供给模式,在云计算时代让普通开发者也能够通过阿里云提供的语音识别云端自学习技术,获得定制优化自己所关心的业务场景的成套手段。阿里云让广大的开发者站在巨头的肩膀上,通过自主可控的自学习,在短时间内实现对语音识别系统应用从入门到精通,并在开发者关心的场景下轻松拥有业界顶尖的语音识别准确率。这就是云计算时代的语音识别技术全新的供给模式。与其它人工智能技术一样,语音识别技术的关键在于算法、算力和数据三个方面。阿里云依托达摩院语音交互智能,近年来持续在世界前沿进行“算法”演进,近期还将最新的研究成果DFSMN声学模型开源,供全世界的研究者复现目前最佳的结果并进行持续提升。在“算力”方面自不用说,这本身就是云计算的天然强项。基于阿里云ODPS-PAI平台,我们构建了专为语音识别应用优化的CPU/GPU/FPGA/NPU训练和服务混布平台,每天服务于阿里云上巨量的语音识别请求。在“数据”方面,我们提供通过海量数据训练的、开箱即用的场景模型,包括电商、客服、政务、手机输入等等。同时应该看到,在具体的落地场景下往往会有一些非常特殊、领域相关的“说法”需要被识别,很多时候类似于“碎屑岩岩性地层”、“海相碳酸盐岩”这种特定说法对于通用场景模型的识别率提出了挑战。要获得开发者关心的具体场景下最佳的准确率,开箱即用的模型一般还需要一定的定制优化工作才可以达到。传统上,这样的定制是通过语音技术服务提供商来完成的,在成本、周期、可控性等方面都存在明显不足。阿里云提供的语音定制“自学习”平台服务,可以提供多种手段,在很短的时间内、以较低的成本,让开发者完全掌控模型定制优化及上线的工作。阿里云创新工具平台及服务技术,依托强大的基础设施,使得在云计算的大背景下进行大规模定制化语音服务成为可能。而开发者完全无需关心后台的技术和服务,只需要使用阿里云提供的简单易用的“自学习”工具,利用场景知识和数据,就可以获得该特定场景下最优的效果,并按需要持续迭代提升。阿里云的智能语音自学习平台具备以下优势:易:智能语音自学习平台颠覆性地提供一键式自助语音优化方案,极大地降低进行语音智能优化所需要的门槛,让不懂技术的业务人员也可以来显著提高自身业务识别准确率。快:自学习平台能够在数分钟之内完成业务专属定制模型的优化测试上线,更能支持业务相关热词的实时优化,一改传统定制优化长达数周甚至数月的漫长交付弊端。准:自学习平台优化效果在很多内外部合作伙伴和项目上得到了充分验证,很多项目最终通过自学习平台不光解决了效果可用性问题,还在项目中超过了竞争对手使用传统优化方式所取得的优化效果。举例来说,开发者可以使用下述多种“自学习”手段来定制自己关心领域的模型:a)业务热词定制在许多特定场所,要求快速对特定词的识别能力进行加强(注:包括两种模式,模式一为其他词易被识别成特定词;模式二为特定词易被识别成其他词),采用实时热词加载技术,可以在实时场景下,通过设置不同的档位,能够实现热词识别能力的加强。b)类热词定制很多时候,相同的发音相同的属性在不同上下文上会需要不同的识别效果。联系人和地名就是典型的案例,对于不同人的好友,“张阳”和“章扬”我们就必须能准确地识别出相应的名字。同样,相隔千里的安溪跟安西如果识别错误会给导航带来大麻烦。智能语音自学习平台相信“每个人都值得被尊重”,提供联系人类和地名类的定制能力,“让天下没有难识的路”。c)业务专属模型定制用户通过输入对应领域的相关文本,如行业或公司的基本介绍、客服聊天记录、领域常用词汇和专有名词等,即可快速自行生成该行业下的定制模型,整个定制过程无需用户人工干预。通过这些手段,阿里云使得开发者不必关心语音技术的算法和工程服务细节,专注于他们擅长的垂直领域的知识和数据收集,实现全新的语音技术云端供给模式,造福于广大的开发者及其业务结果。本文作者:鄢志杰、薛少飞、张仕良、郑昊、雷鸣阅读原文本文来自云栖社区合作伙伴“阿里技术”,如需转载请联系原作者。

January 15, 2019 · 1 min · jiezi

[NodeJs系列]NodeJs模块机制

注: 1. 本文涉及的nodejs源码如无特别说明则全部基于v10.14.1欢迎关注公众号:前端情报局Nodejs 中对模块的实现本节主要基于NodeJs源码,对其模块的实现做一个简要的概述,如有错漏,望诸君不吝指正。当我们使用require引入一个模块的时候,概况起来经历了两个步骤:路径分析和模块载入路径分析路径分析其实就是模块查找的过程,由_resolveFilename函数实现。我们通过一个例子,展开说明:const http = require(‘http’);const moduleA = requie(’./parent/moduleA’);这个例子中,我们引入两种不同类型的模块:核心模块-http和自定义模块moduleA对于核心模块而言,_resolveFilename会跳过查找步骤,直接返回,交给下一步处理if (NativeModule.nonInternalExists(request)) { // 这里的request 就是模块名称 ‘http’ return request;}而对于自定义模块而言,存在以下几种情况(_findPath)文件模块目录模块从node_modules目录加载全局目录加载这些在官方文档中已经阐述的很清楚了,这里就不再赘述。如果模块存在,那么_resolveFilename会返回该模块的绝对路径,比如/Users/xxx/Desktop/practice/node/module/parent/moduleA.js。载入模块获取到模块地址后,Node就开始着手载入模块。首先,Node会查看模块是否存在缓存中:// filename 即模块绝对路径var cachedModule = Module._cache[filename];if (cachedModule) { updateChildren(parent, cachedModule, true); return cachedModule.exports;}存在则返回对应缓存内容,不存在则进一步判断该模块是否是核心模块:if (NativeModule.nonInternalExists(filename)) { return NativeModule.require(filename);}如果模块既不存在于缓存中也非核心模块,那么Node会实例化一个全新的模块对象function Module(id, parent){ // 通常是模块绝对路径 this.id = id; // 要导出的内容 this.exports = {}; // 父级模块 this.parent = parent; this.filename = null; // 是否已经加载成功 this.loaded = false; // 子模块 this.children = [];}var module = new Module(filename, parent);而后Node会根据路径尝试载入。function tryModuleLoad(module, filename) { var threw = true; try { module.load(filename); threw = false; } finally { if (threw) { delete Module._cache[filename]; } }}对于不同的文件扩展名,其载入方法也有所不同。.js文件(_compile)通过fs同步读取文件内容后将其包裹在指定函数中:Module.wrapper = [ ‘(function (exports, require, module, __filename, __dirname) { ‘, ‘\n});’];调用执行此函数:compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);.json文件通过fs同步读取文件内容后,用JSON.parse解析并返回内容var content = fs.readFileSync(filename, ‘utf8’);try { module.exports = JSON.parse(stripBOM(content));} catch (err) { err.message = filename + ‘: ’ + err.message; throw err;}.node这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。return process.dlopen(module, path.toNamespacedPath(filename));.mjs这是用于处理ES6模块的扩展文件,是NodeJs在v8.5.0后新增的特性。对于这类扩展名的文件,只能使用ES6模块语法import引入,否则将会报错(启用 –experimental-modules的情况下)throw new ERR_REQUIRE_ESM(filename);如果一切顺利,就会返回附加在exports对象上的内容return module.exports;模块循环依赖接下来我们来探究一下模块循环依赖的问题:模块1依赖模块2,模块2依赖模块1,会发生什么?这里只探究commonjs的情况为此,我们创建了两个文件,module-a.js和module-b.js,并让他们相互引用:module-a.jsconsole.log(’ 开始加载 A 模块’);exports.a = 2;require(’./module-b.js’);exports.b = 3;console.log(‘A 模块加载完毕’);module-b.jsconsole.log(’ 开始加载 B 模块’);let moduleA = require(’./module-a.js’);console.log(moduleA.a,moduleA.b)console.log(‘B 模块加载完毕’);运行module-a.js,可以看到控制台输出:开始加载 A 模块开始加载 B 模块2 undefinedB 模块加载完毕A 模块加载完毕这时因为每个require都是同步执行的,在module-a完全加载前需要先加载./module-b,此时对于module-a而言,其exports对象上只附加了属性a,属性b是在./module-b加载完成后才赋值的。QA如何删除模块缓存?可以通过delete require.cache(moduleId)来删除对应模块的缓存,其中moduleId表示的是模块的绝对路径,一般的,如果我们需要对某些模块进行热更新,可以使用此特性,举个例子:// hot-reload.jsconsole.log(’this is hot reload module’);// index.jsconst path = require(‘path’);const fs = require(‘fs’);const hotReloadId = path.join(__dirname,’./hot-reload.js’);const watcher = fs.watch(hotReloadId);watcher.on(‘change’,(eventType,filename)=>{ if(eventType === ‘change’){ delete require.cache[hotReloadId]; require(hotReloadId); }});Node中可以使用ES6 模块吗?从8.5.0版本开始,NodeJs开始支持原生ES6模块,启用该功能需要两个条件:所有使用ES6模块的文件扩展名都必须是.mjs命令行选项–experimental-modulesnode –experimental-modules index.mjsnode –experimental-modules index.mjs但是截止到NodeJs v10.15.0,ES6模块的支持依旧是实验性的,笔者并不推荐在公司项目中使用参考nodejs-loader.js朴灵. 深入浅出Node.js ...

January 14, 2019 · 1 min · jiezi

基于Kubernetes 的机器学习工作流

介绍Pipeline是Kubeflow社区最近开源的一个端到端工作流项目,帮助我们来管理,部署端到端的机器学习工作流。Kubeflow 是一个谷歌的开源项目,它将机器学习的代码像构建应用一样打包,使其他人也能够重复使用。 kubeflow/pipeline 提供了一个工作流方案,将这些机器学习中的应用代码按照流水线的方式编排,形成可重复的工作流。并提供平台,帮助编排,部署,管理,这些端到端机器学习工作流。概念pipeline 是一个面向机器学习的工作流解决方案,通过定义一个有向无环图描述流水线系统(pipeline),流水线中每一步流程是由容器定义组成的组件(component)。 当我们想要发起一次机器学习的试验时,需要创建一个experiment,在experiment中发起运行任务(run)。Experiment 是一个抽象概念,用于分组管理运行任务。Pipeline:定义一组操作的流水线,其中每一步都由component组成。 背后是一个Argo的模板配置。Component: 一个容器操作,可以通过pipeline的sdk 定义。每一个component 可以定义定义输出(output)和产物(artifact), 输出可以通过设置下一步的环境变量,作为下一步的输入, artifact 是组件运行完成后写入一个约定格式文件,在界面上可以被渲染展示。Experiment: 可以看做一个工作空间,管理一组运行任务。Run: pipeline 的运行任务实例,这些任务会对应一个工作流实例。由Argo统一管理运行顺序和前后依赖关系。Recurring run: 定时任务,定义运行周期,Pipeline 组件会定期拉起对应的Pipeline Run。Pipeline 里的流程图组件的Artifact模块Pipeline 的组件比较简单,大致分为5个部分。MySQL: 用于存储Pipeline/Run 等元数据。Backend: 一个由go编写的后端,提供kubernetes ApiServer 风格的Restful API。处理前端以及SDK发起的操作请求。 Pipeline/Experiment 之类的请求会直接存入MySQL元数据。和Run 相关的请求除了写入MySQL以外还会通过APIServer 同步操作Argo实例。CRD Controller: Pipeline 基于Argo扩展了自己的CRD ScheduledWorkflow, CRD Controller 中会主要监听ScheduledWorkflow和Argo 的Workflow 这两个CRD变化。处理定期运行的逻辑。Persistence Agent: 和CRD Controller 一样监听Argo Workflow变化,将Workflow状态同步到MySQL 元数据中。它的主要职责是实时获取工作流的运行结果。Web UI:提供界面操作。 从Backend 中读取元数据,将流水线过程和结果可视化,获取日志,发起新的任务等。其他工具除了以上核心模块以外, Pipeline提供了一系列工具,帮助更好构建流水线。SDK, 用于定义pipeline和component,编译为一个argo yaml模板,可以在界面上导入成pipeline。CLI 工具,替代Web UI,调用Backend Api 管理流水线Jupyter notebook。 可以在notebook中编写训练代码,也可以在notebook中通过sdk管理Pipeline。本文作者:萧元阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 3, 2019 · 1 min · jiezi

短视频宝贝=慢?阿里巴巴工程师这样秒开短视频

前言随着短视频兴起,各大APP中短视频随处可见,feeds流、详情页等等。怎样让用户有一个好的视频观看体验显得越来越重要了。大部分feeds里面滑动观看视频的时候,有明显的等待感,体验不是很好。针对这个问题我们展开了一波优化,目标是:视频播放秒开,视频播放体验良好。无图无真相,上个对比图,左边是优化之前的,右边是优化之后的:问题分析视频格式的选择在正式分析问题之前有必要说明下:我们现在首页的视频,都是320p H.264编码的mp4视频。H.264 & H.265H.264也称作MPEG-4AVC(Advanced Video Codec,高级视频编码),是一种视频压缩标准,同时也是一种被广泛使用的高精度视频的录制、压缩和发布格式。H.264因其是蓝光光盘的一种编解码标准而著名,所有蓝光播放器都必须能解码H.264。H.264相较于以前的编码标准有着一些新特性,如多参考帧的运动补偿、变块尺寸运动补偿、帧内预测编码等,通过利用这些新特性,H.264比其他编码标准有着更高的视频质量和更低的码率.H.265/HEVC的编码架构大致上和H.264/AVC的架构相似,也主要包含:帧内预测(intra prediction)、帧间预测(inter prediction)、转换 (transform)、量化 (quantization)、去区块滤波器(deblocking filter)、熵编码(entropy coding)等模块。但在HEVC编码架构中,整体被分为了三个基本单位,分别是:编码单位(coding unit,CU)、预测单位(predict unit,PU) 和转换单位(transform unit,TU )。总的来说H.265压缩效率更高,传输码率更低,视频画质更优。看起来使用H.265似乎是很明智的选择,但我们这里选择的是H.264。原因是:H.264支持的机型范围更为广泛。 PS:闲鱼H.265视频在宝贝详情页会在近期上线,敬请关注体验!TS & FLV & MP4TS是日本高清摄像机拍摄下进行的封装格式,全称为MPEG2-TS。TS即"Transport Stream"的缩写。MPEG2-TS格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。下述命令可以把mp4转换成ts格式,从结果来看ts文件(4.3MB)比mp4文件(3.9MB)大10%左右。ffmpeg -i input.mp4 -c copy output.tsFLV是FLASH VIDEO的简称,FLV流媒体格式是随着Flash MX的推出发展而来的视频格式。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入Flash后,使导出的SWF文件体积庞大,不能在网络上很好的使用等问题。FLV只支持一个音频流、一个视频流,不能在一个文件里包含多路音频流。音频采样率不支持48k,视频编码不支持H.265。相同编码格式下,文件大小和mp4几乎没有区别。 ffmpeg -i input.mp4 -c copy output.flv MP4是为大家所熟知的一种视频封装格式,MP4或称MPEG-4第14部分是一种标准的数字多媒体容器格式。MPEG-4第14部分的扩充名为.mp4,以存储数字音频及数字视频为主,但也可以存储字幕和静止图像。因其可容纳支持比特流的视频流,MP4可以在网络传输时使用流式传输。其兼容性很好,几乎所有的移动设备都支持,而且还能在浏览器、桌面系统进行播放。综合上面几个封装格式的特点,我们的最终选择是MP4.播放流程一个视频在客户端的播放流程是怎么样的?播放首开慢耗时在什么地方?耗时点是否能够快速低成本的解决?了解视频的播放流程有助于找到问题的突破口。视频从加载到播放可以分为三个阶段:读取(IO):“获取” 内容 -> 从 “本地” or “服务器” 上获取解析(Parser):“理解” 内容 -> 参考 “格式&协议” 来 “理解” 内容渲染(Render):“展示” 内容 -> 通过扬声器/屏幕来 “展示” 内容可以看出内容获取从“服务器”改为“本地”,这样会节省很大一部分时间,而且成本很低,是一个很好的切入点。事实也是如此,我们的优化正是围绕此点展开。PS: 我们使用的网络库,播放器都是集团内部的,本身做了很多优化。本文不涉及网络协议,播放器方面的优化讨论。技术方案鉴于上面的分析,我们要做的工作是:把mp4文件提前缓存一部分,到feeds滑动要播放的时候,播放本地的mp4文件。由于用户可能继续观看视频,所以本地的数据播放完后,需要从网络下载数据进行播放。这里需要解决两个问题:应该提前下载多少数据缓存数据播放完成后该怎么切换到网络数据MOOV BOX的位置对于第一个问题,我们不得不分析一下mp4的文件结构,看看我们应该下载多少数据量合适。MP4是由很多Box 组成的,Box里面可以嵌套Box:这里不详细介绍MP4的格式信息。但是可以看出moov box对播放很关键,它提供的信息如:宽高、时长、码率、编码格式、帧列表、关键帧列表等等。播放器没有获取到moov box是没办法进行播放的。所以下载的数据应该要包含moov box再加上几十帧的数据。做了一个简单的计算:闲鱼短视频一般最长是30s,feeds里面的分辨率是320p,码率是1141kb/s,ftyp+moov这个视频的数据量在31kb左右(打开文件可以看出mdat是从31754byte的位置开始的),所以,头部信息+10帧的数据大约是:(31kb + 1141kb/3)/8 = 51KBProxy第二个问题:缓存数据播放完成后该怎么切换到网络数据呢?在本地数据播放完成之后,设置一个网络地址给播放器,告诉播放器下载的offset是多少,然后继续从网络下载数据播放。这样看起来可行,但是需要播放器提供支持:本地数据播放完成的回调;设置网络url并支持offset。另外,服务端需要支持range参数,而且切换到网络播放的时候需要新建立网络连接,很可能会造成卡顿。最终,我们选择了proxy的方式,把proxy作为中间人,负责预加载数据、给播放器提供数据,切换逻辑在proxy里面来完成。未加入proxy之前流程是这样的:加入了proxy之后流程是这样的:这样做的好处很明显,我们可以在proxy里面做很多事情:例如本地文件缓存数据和网络数据的切换工作。甚至是和CDN使用其它的协议进行通信。我们这里假定预加载工作已经完成,看看播放器是怎么和proxy进行交互的。播放的时候会用Proxy提供的一个localhost的url进行播放,这样代理服务器会收到网络请求,把本地预加载的数据返回给播放器。播放器完全感知不到proxy模块、预加载模块的存在。播放器、预加载模块都是Proxy的client,调用逻辑都是一样。图示说明如下:下面逐步解释一下,数据的加载过程:Client发起http请求获取数据,箭头1所示文件缓存如果存在所请求的数据则直接返回数据,箭头2所示若本地文件缓存数据不够,则发起网络请求,向CDN请求数据,箭头3所示获取网络数据,写入文件缓存,箭头4所示返回请求的数据给Client,箭头2所示实现模块预加载模块确定了技术方案后,预加载模块还是有很多工作要做的。在列表网络数据解析完成后会触发视频预加载,首先会根据url生成md5值,然后去查看这个md5值对应的任务是否存在,如果存在则不会重复提交。生成任务后会提交到线程池,在后台线程进行处理。网络从Wifi切换到3G的时候,会把任务取消,防止消耗用户的数据流量。预加载任务在线程池执行的时候,其流程是这样的:首先会获取一个本地代理的url。然后发起http请求。Proxy会收到http请求进行处理,开始做真正的数据预加载工作。预加载模块读取到指定的数据量后终止。到此,预加载的任务就已完成。流程图如下所示:在用户快速滑动的时候,怎么能保证视频还能继续秒开呢?预加载模块对于每一个任务都会维护一个状态机,在Fling的时候会把划过的任务暂停下,把最新要显示的任务优先级提高,让其优先执行。Proxy模块Proxy内部有个local的httpServer负责拦截播放器和预加载模块的http请求。client在请求时会带入CDN的url,在本地缓存数据没有的时候会去CDN获取新鲜数据。因为有多个地方向Proxy请求数据,所以用线程池来处理多个client的连接很有必要,这样多个client可以并行,不会因为前面有client在请求而阻塞。文件缓存使用LruDiskCache,在超过指定文件大小后,老的缓存文件会删除,这是一个在使用文件缓存时很容易忽视的问题。由于我们的场景视频是连续播放的,不存在seek的情况,所以文件缓存相对比较简单,不用考虑文件分段的情况。Proxy内部对于同一个url会映射到一个client,如果预加载和播放同时进行,数据只会有一份,不会去重复下载数据。再来一个Proxy内部构造示意图:遇到的问题在测试中发现,有的视频还是会播放很慢,仔细查看本地的确缓存了期望的数据大小,但是播放的时候还是有较长的等待时间,这种视频有个特点:moov box在尾部。对于moov在尾部的视频,是整个文件都下载完成后才进行播放的,原因是moov box里面存了很多关键信息,前面分析mp4格式的时候有提到。对于这个问题有两个解法:解法一:服务端在进行转码的时候保证moov的头部在前面,发现moov位置不正确的视频服务端进行订正。PS:查看moov在文件中的位置可以用hex文本编辑器打开,按字符搜索moov所在的位置即可,MAC上面还可以使用MediaParser , 另外还可以用ffmpeg命令生成moov在头部或者尾部的mp4文件。例如: 从1.mp4 copy一个文件,使其moov头在尾部ffmpeg -i 1.mp4 -c copy -f mp4 output.mp4 从1.mp4 copy一个文件,使其moov头在头部:ffmpeg -i 1.mp4 -c copy -f mp4 -movflags faststart output2.mp4解法二不用修改moov box的位置,而是在播放端进行处理,播放端需要检测流信息,如果moov前面没有,就去请求文件的尾部信息。具体就是:发起 HTTP MP4 请求,读取响应 body 的开头,如果发现 moov 在开头,就接着往下读mdat。如果发现开头没有,先读到 mdat,马上 RESET 这个连接,然后通过 Range 头读取文件末尾数据,因为前面一个 HTTP 请求已经获取到了 Content-Length ,知道了 MP4 文件的整个大小,通过 Range 头读取部分文件尾部数据也是可以的。示意图如下这个方案的缺点是:对于moov box在尾部的视频会多两次http connection。结语本文介绍了常见的视频编码格式,视频封装格式,介绍了moov头信息对于视频播放的影响。随着对于播放流程的分析,我们找到了问题的切入点。简单说就是围绕着数据预加载展开,把网络请求数据的工作提前完成,播放的时候直接从缓存读取,而且后续的视频回看都是从缓存读取,不仅解决了视频初始化播放慢的问题,还解决了播放缓存问题,可以说是一箭双雕。Proxy是这个方案的核心思想,本地localhost的url是一个关键纽带,视频预加载模块和播放器模块解耦彻底,换了播放器照样可以使用。到此为止,视频feeds秒开优化就已完成。上线后的数据来看视频打开速度在800ms左右。回过头来,或许我们还可以更进一步,可以对预加载收到的数据进行验证,确保缓存了准确的信息,而不是固定的数值。还可以进行更加深度的优化,让用户观看视频的体验更加顺滑。参考文献* AndroidVideoCache* 视频的封装格式和编码格式* 播放器技术分享(1):架构设计* MP4文件格式的解析,以及MP4文件的分割算法* 从天猫某活动视频3次请求说起* [视音频编解码学习工程:FLV封装格式分析器]https://blog.csdn.net/leixiaohua1020/article/details/17934487* https://www.adobe.com/content/dam/acom/en/devnet/flv/video_file_format_spec_v10_1.pdf* https://baike.baidu.com/item/flv* https://standards.iso.org/ittf/PubliclyAvailableStandards/index.html本文作者:闲鱼技术-邻云阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 21, 2018 · 1 min · jiezi

如何去设计前端框架能力?星巴克消息开放项目从0到1,从点到面的思考

本文由淘宝前端工程师罗嗣分享,主要讲述了作者在星巴克消息开放项目中的总结和思考,希望对大家有帮助,让业务分享更加有价值。摘要从满足星巴克项目需求单点出发,发散到从点到面的思考。从而总结了自己思考的基本流程(方法论)。从如下四个递进方面思考。业务拓展:拓展自有业务的边界,和其他业务合作共建,形成标准的能力透出, 合力共建。业务趋势:业务的特点和趋势是如何。技术可以如何储备来应对未来业务的变化。技术趋势:技术命题,技术趋势。选择适合的技术来解决现在的问题。保持技术对未来的弹性。需求问题:客观存在的事实,现在需求存在哪些问题,我们如何去帮助业务更加稳定,更加高效。本文提纲笔者从0到1构建一个IM前端系统,再从点到面思考整合突破原有的自有业务限制,尽量设计出的可扩展,可交互,甚至小而美的系统能力。本文会从如下几个方面去介绍。点:项目背景及需求难点(支付宝星巴克小程序入驻客服接待),以及现有的能力。面:需求做完反向思考,当前BC/CC遇到的问题及痛点,如何在同一个领域模型下做推动标准化能力。需求介绍项目背景客服接待能力由手淘消息平台和CCO团队合作共建,整体采用AMP+XSPACE的方案落地,AMP承接C端用户聊天界面,XSPACE承接B端聊天界面,同时接待又需要原有BC的聊天能力。星巴克客服接待两纵一横,底部需要对接不同的服务端,上层需要保证同一套UI来提升一致性体验。设计思路总体设计思想:设计分离出数据层和UI层,数据层和UI层以标准化协议对接。这样分层就可以解决当前业务遇到的问题,如下是当时需求的标准SDK事例点到面的思考星巴克客服消息接待开放是一种轻量级(H5形式)的客服接入能力。思考当前业务的问题是什么,如何改进,业务价值的意义等。 笔者会从如下几个方面去思考。原有H5旺旺由于历史原因有稳定性和体验的问题,这套方案能不能提供替换成原来的H5旺旺,同时对聊天接入统一收口(标准化组件)。从而达到更加稳定,更加的体验性。H5旺旺聊天可以投放到阿里系的其他端上(优酷,饿了嘛,拍卖等),甚至现在很多外投的广告业务。把H5聊天能力做强对淘宝的引流及成交都有很大的意义。同时集团里面还有小蜜作为客服聊天能力。能不能站在前端的角度思考整合输出。针对集团二方业务。需要定制个性化消息和UI能力,需要把SDK能力提供给他们去进行上层业务扩展,为保证他们低成本的接入需要提供基础能力,二方去扩展插件。同时工具链路上需要保证提高效率。生成闭环的开发环境,接入业务方只要关系自己的业务需求思考模型基于之前的背景和诉求,整体设计思路: 抽离UI层和数据层(模块),UI层和数据层基于Message实体进行标准化协议对接(桥梁)。工具链路垂直支持提高效能。 有如下几个方面衔接点:开放 UI组件 和 标准化SDK能力,让二方业务快速搭建,UI层 和 数据层之间用 标准化协议做桥梁连接在基础SDK上,会透出Context上下文(内部核心对象message,session,app)让业务去定制修改,业务方只需要去扩展插件。基于DEF脚手架体系提供相应工具链路支持,包括项目模板生成,项目测试,项目构建,完善可持续集成。业务价值在阿里做每件事情,需要明确这件事情的价值,这件事情投入产出比是多少。上文也陆续在提价值。 如图可以说明这件事价值实践方案上面几章介绍了项目背景,设计思路,思考模型和业务价值(PS:类似于论文前两章在介绍背景和理论知识)。这章主要是讲的项目实践。站在前端的角度,从四个方面去实践,并付相应代码地址。标准化协议: 由于消息领域模型是一致的,可以抽象出标准的 会话和 消息 格式。他是SDK和组件能力的桥梁SDK能力开放:提供标准化数据对接的能力,负责插件扩展能力。 业务入驻只需要开发业务相应的中间件(插件)。例如:各自业务的编解码模块,登录模块,消息处理模块组件能力开放:提供标准化的聊天能力组件。例如聊天入口接入标准化组件工具链路支撑:基于DEF脚手架体系,开发了def-kit-tbms套件。支撑项目全链路开发领域模型(标准化协议)为了达到消息标准化能力,需要把基本概念和接口达成一致。梳理两个基础概念: 会话 和 消息。会话conversation: 它是指AB通讯之间维持的一种关系,它是消息存储的载体消息message: 消息是一个对话的基本组成部分, 根据业务分为两大块消息,会话内消息和系统通知消息。会话内消息又可以分为基本消息和自定义消息。会话类型即时通讯 SDK 的核心概念「会话」,即 Conversation。我们将单聊和群聊(包括聊天室)的消息发送和接收都依托于 Conversation 这个统一的概念进行操作。会话属性备注id会话IDscene场景to聊天对象,账号或者群IDupdateTime会话更新时间unread未读数lastMsg此会话的最后一条消息custom扩展Json字符串消息类型IM SDK内的消息可以分为两类:会话内消息和系统通知消息。会话内消息只能出现并展示在聊天界面里,一般是应用内的一个用户发给另一个用户(或群组/聊天室)的消息,例如文本消息、图片消息都属于会话内消息。:会话内消息类型备注文本消息消息内容为普通文本图片消息消息内容为图片URL地址、尺寸、图片大小等信息语音消息消息内容为语音URL地址、时长、大小、格式等信息视频消息消息内容为视频文件的URL地址、时长、大小、格式等信息文件消息消息内容为文件的URL地址、大小、格式等信息,格式不限地理位置消息消息内容为地理位置标题、经度、纬度信息通知消息自定义消息可以用于消息接入扩展。 例如卡片消息,红包消息等。自定义消息**通知消息属于会话内的一种消息,用于会话内通知和提示场景。例如:群名称更新、某某某退出了群聊等。**会话和消息标准化格式标准化协议标准化会话格式标准化消息格式SDK能力开放SDK的设计参考了Koajs的设计原理(底层微创新了下)。Koajs的中间件思路: 中间件对于一次请求来处理,context分别集成了request和response对象, 同理可以映射成对一条收发消息的处理,面向切面的编程方式。。 在context中会集成message(消息),session(会话),app(如用户,初始化sdk信息等其他信息)。整个项目通过lerna进行了包管理,用Typescript写了SDK,并做了充分的单元测试,大家可以放心使用。整个项目分为了如下几个模块:@ali/tbms-compose: 函数组合模块,用于@ali/tbms-middlware服务@ali/tbms-middleware: 中间件模块@ali/tbms-util: 通用函数分装:如promise同步执行队列,mtop请求,event事件系统@ali/tbms-sdk: 消息标准化基础SDK,可以让业务扩展,补充插件测试说明:对底层支持的SDK都做了充分的单元测试,保证稳定性。后续版本更新提供差异性修改的检查使用事例组件能力开放由于需要多端投放,某些二方应用支持weex能力。从而选择了RAX技术方案。再在H5表现下对单聊做性能优化,现阶段完成聊天入口的统一接入组件,上层的组件在陆续完善中(完成度20%)。@ali/rax-tbms-chatwater tbms-components工具链路支撑基于DEF脚手架体系,开发了def-kit-tbms套件。提供项目全链路开发支撑。这个项目后续的项目搭建都采用standard-dev脚手架生成项目目录。例如:tbms-toolkit,tbms-packages总结这是一次完整的一个项目从0到1,从点到面的思考过程,建立模型到付诸于实践。从完成业务需求(需求做什么)到帮助业务成长(我能做什么)的思考过程。虽然只是站在前端角度在思考问题,但是方法论相信可以通用。后续Action改善原来的H5旺旺,使之更加稳定和更好的体验性。同时对聊天接入统一收口(标准化组件和标准化接入SDK)。Flag:利用业余时间,一月中旬前第一版本里程碑发布未完待续有什么IM相关的需求都可以联系我@罗嗣,共建标准化和生态。本文作者:罗嗣阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 19, 2018 · 1 min · jiezi

用简单代码看卷积组块发展

摘要: 在本文中,我想带领大家看一看最近在Keras中实现的体系结构中一系列重要的卷积组块。作为一名计算机科学家,我经常在翻阅科学技术资料或者公式的数学符号时碰壁。我发现通过简单的代码来理解要容易得多。因此,在本文中,我想带领大家看一看最近在Keras中实现的体系结构中一系列重要的卷积组块。当你在GitHub网站上寻找常用架构实现时,一定会对它们里面的代码量感到惊讶。如果标有足够多的注释并使用额外的参数来改善模型,将会是一个非常好的做法,但与此同时这也会分散体系结构的本质。为了进一步简化和缩短代码,我将使用一些别名函数:defconv(x, f, k=3, s=1, p=‘same’, d=1, a=‘relu’): return Conv2D(filters=f, kernel_size=k, strides=s, padding=p, dilation_rate=d, activation=a)(x)def dense(x, f, a=‘relu’): return Dense(f, activation=a)(x)defmaxpool(x, k=2, s=2, p=‘same’): return MaxPooling2D(pool_size=k, strides=s, padding=p)(x)defavgpool(x, k=2, s=2, p=‘same’): return AveragePooling2D(pool_size=k, strides=s, padding=p)(x)defgavgpool(x): return GlobalAveragePooling2D()(x)defsepconv(x, f, k=3, s=1, p=‘same’, d=1, a=‘relu’): return SeparableConv2D(filters=f, kernel_size=k, strides=s, padding=p, dilation_rate=d, activation=a)(x)在删除模板代码之后的代码更易读。当然,这只有在你理解我的首字母缩写后才有效。defconv(x, f, k=3, s=1, p=‘same’, d=1, a=‘relu’): return Conv2D(filters=f, kernel_size=k, strides=s, padding=p, dilation_rate=d, activation=a)(x)def dense(x, f, a=‘relu’): return Dense(f, activation=a)(x)defmaxpool(x, k=2, s=2, p=‘same’): return MaxPooling2D(pool_size=k, strides=s, padding=p)(x)defavgpool(x, k=2, s=2, p=‘same’): return AveragePooling2D(pool_size=k, strides=s, padding=p)(x)defgavgpool(x): return GlobalAveragePooling2D()(x)defsepconv(x, f, k=3, s=1, p=‘same’, d=1, a=‘relu’): return SeparableConv2D(filters=f, kernel_size=k, strides=s, padding=p, dilation_rate=d, activation=a)(x)瓶颈(Bottleneck)组块一个卷积层的参数数量取决于卷积核的大小、输入过滤器的数量和输出过滤器的数量。你的网络越宽,3x3卷积耗费的成本就越大。def bottleneck(x, f=32, r=4): x = conv(x, f//r, k=1) x = conv(x, f//r, k=3) return conv(x, f, k=1)瓶颈组块背后的思想是,使用一个低成本的1x1卷积,按照一定比率r将通道的数量降低,以便随后的3x3卷积具有更少的参数。最后,我们用另外一个1x1的卷积来拓宽网络。Inception模块模块提出了通过并行的方式使用不同的操作并且合并结果的思想。通过这种方式网络可以学习不同类型的过滤器。defnaive_inception_module(x, f=32): a = conv(x, f, k=1) b = conv(x, f, k=3) c = conv(x, f, k=5) d = maxpool(x, k=3, s=1) return concatenate([a, b, c, d])在这里,我们将使用卷积核大小分别为1、3和5的卷积层与一个MaxPooling层进行合并。这段代码显示了Inception模块的原始实现。实际的实现结合了上述的瓶颈组块思想,这使它稍微的复杂了一些。definception_module(x, f=32, r=4): a = conv(x, f, k=1) b = conv(x, f//3, k=1) b = conv(b, f, k=3) c = conv(x, f//r, k=1) c = conv(c, f, k=5) d = maxpool(x, k=3, s=1) d = conv(d, f, k=1) return concatenate([a, b, c, d])剩余组块(ResNet)ResNet是由微软的研究人员提出的一种体系结构,它允许神经网络具有任意多的层数,同时还提高了模型的准确度。现在你可能已经习惯使用它了,但在ResNet之前,情况并非如此。defresidual_block(x, f=32, r=4): m = conv(x, f//r, k=1) m = conv(m, f//r, k=3) m = conv(m, f, k=1) return add([x, m])ResNet的思路是将初始的激活添加到卷积组块的输出结果中。利用这种方式,网络可以通过学习过程决定用于输出的新卷积的数量。值得注意的是,Inception模块连接这些输出,而剩余组块是用于求和。ResNeXt组块根据它的名称,你可以猜到ResNeXt与ResNet是密切相关的。作者们将术语“基数(cardinality)”引入到卷积组块中,作为另一个维度,如宽度(通道数量)和深度(网络层数)。基数是指在组块中出现的并行路径的数量。这听起来类似于以并行的方式出现的4个操作为特征的Inception模块。然而,基数4不是指的是并行使用不同类型的操作,而是简单地使用相同的操作4次。它们做的是同样的事情,那么为什么你还要把它们并行放在一起呢?这个问题问得好。这个概念也被称为分组卷积,可以追溯到最早的AlexNet论文。尽管当时它主要用于将训练过程划分到多个GPU上,而ResNeXt则使用ResNeXt来提高参数的效率。defresnext_block(x, f=32, r=2, c=4): l = [] for i in range(c): m = conv(x, f//(cr), k=1) m = conv(m, f//(cr), k=3) m = conv(m, f, k=1)l.append(m) m = add(l) return add([x, m])这个想法是把所有的输入通道分成一些组。卷积将只会在其专用的通道组内进行操作,而不会影响其它的。结果发现,每组在提高权重效率的同时,将会学习不同类型的特征。想象一个瓶颈组块,它首先使用一个为4的压缩率将256个输入通道减少到64个,然后将它们再恢复到256个通道作为输出。如果想引入为32的基数和2的压缩率,那么我们将使用并行的32个1x1的卷积层,并且每个卷积层的输出通道是4(256/(322))个。随后,我们将使用32个具有4个输出通道的3x3的卷积层,然后是32个1x1的卷积层,每个层则有256个输出通道。最后一步包括添加这32条并行路径,在为了创建剩余连接而添加初始输入之前,这些路径会为我们提供一个输出。这有不少的东西需要消化。用上图可以非常直观地了解都发生了什么,并且可以通过复制这些代码在Keras中自己创建一个小型网络。利用上面9行简单的代码可以概括出这些复杂的描述,这难道不是很好吗?顺便提一下,如果基数与通道的数量相同,我们就会得到一个叫做深度可分卷积(depthwise separable convolution)的东西。自从引入了Xception体系结构以来,这些技术得到了广泛的应用。密集(Dense)组块密集组块是剩余组块的极端版本,其中每个卷积层获得组块中之前所有卷积层的输出。我们将输入激活添加到一个列表中,然后输入一个可以遍历块深度的循环。每个卷积输出还会连接到这个列表,以便后续迭代获得越来越多的输入特征映射。这个方案直到达到了所需要的深度才会停止。defdense_block(x, f=32, d=5): l = x for i in range(d): x = conv(l, f) l = concatenate([l, x]) return l尽管需要数月的研究才能得到一个像DenseNet这样出色的体系结构,但是实际的构建组块其实就这么简单。SENet(Squeeze-and-Excitation)组块SENet曾经在短期内代表着ImageNet的较高水平。它是建立在ResNext的基础之上的,主要针对网络通道信息的建模。在常规的卷积层中,每个通道对于点积计算中的加法操作具有相同的权重。SENet引入了一个非常简单的模块,可以添加到任何现有的体系结构中。它创建了一个微型神经网络,学习如何根据输入对每个过滤器进行加权。正如你看到的那样,SENet本身不是一个卷积组块,但是因为它可以被添加到任何卷积组块中,并且可能会提高它的性能,因此我想将它添加到混合体中。defse_block(x, f, rate=16): m = gavgpool(x) m = dense(m, f // rate) m = dense(m, f, a=‘sigmoid’) return multiply([x, m])每个通道被压缩为一个单值,并被馈送到一个两层的神经网络里。根据通道的分布情况,这个网络将根据通道的重要性来学习对其进行加权。最后,再用这个权重跟卷积激活相乘。SENets只用了很小的计算开销,同时还可能会改进卷积模型。在我看来,这个组块并没有得到应有的重视。NASNet标准单元这就是事情变得丑陋的地方。我们正在远离人们提出的简捷而有效的设计决策的空间,并进入了一个设计神经网络体系结构的算法世界。NASNet在设计理念上是令人难以置信的,但实际的体系结构是比较复杂的。我们所了解的是,它在ImageNet上表现的很优秀。通过人工操作,作者们定义了一个不同类型的卷积层和池化层的搜索空间,每个层都具有不同的可能性设置。他们还定义了如何以并行的方式、顺序地排列这些层,以及这些层是如何被添加的或连接的。一旦定义完成,他们会建立一个基于递归神经网络的强化学习(Reinforcement Learning,RL)算法,如果一个特定的设计方案在CIFAR-10数据集上表现良好,就会得到相应的奖励。最终的体系结构不仅在CIFAR-10上表现良好,而且在ImageNet上也获得了相当不错的结果。NASNet是由一个标准单元(Normal Cell)和一个依次重复的还原单元(Reduction Cell)组成。defnormal_cell(x1, x2, f=32): a1 = sepconv(x1, f, k=3) a2 = sepconv(x1, f, k=5) a = add([a1, a2]) b1 = avgpool(x1, k=3, s=1) b2 = avgpool(x1, k=3, s=1) b = add([b1, b2]) c2 = avgpool(x2, k=3, s=1) c = add([x1, c2]) d1 = sepconv(x2, f, k=5) d2 = sepconv(x1, f, k=3) d = add([d1, d2]) e2 = sepconv(x2, f, k=3) e = add([x2, e2]) return concatenate([a, b, c, d, e])这就是如何在Keras中实现一个标准单元的方法。除了这些层和设置结合的非常有效之外,就没有什么新的东西了。倒置剩余(Inverted Residual)组块到现在为止,你已经了解了瓶颈组块和可分离卷积。现在就把它们放在一起。如果你做一些测试,就会注意到,因为可分离卷积已经减少了参数的数量,因此进行压缩可能会损害性能,而不是提高性能。作者们提出了与瓶颈组块和剩余组块相反的想法。他们使用低成本的1x1卷积来增加通道的数量,因为随后的可分离卷积层已经大大减少了参数的数量。在把通道添加到初始激活之前,降低了通道的数量。definv_residual_block(x, f=32, r=4): m = conv(x, fr, k=1) m = sepconv(m, f, a=‘linear’) return add([m, x])问题的最后一部分是在可分离卷积之后没有激活函数。相反,它直接被添加到了输入中。这个组块被证明当被放到一个体系结构中的时候是非常有效的。AmoebaNet标准单元利用AmoebaNet,我们在ImageNet上达到了当前的最高水平,并且有可能在一般的图像识别中也是如此。与NASNet类似,AmoebaNet是通过使用与前面相同的搜索空间的算法设计的。唯一的纠结是,他们放弃了强化学习算法,而是采用了通常被称为“进化”的遗传算法。但是,深入了解其工作方式的细节超出了本文的范畴。故事的结局是,通过进化,作者们能够找到一个比NASNet的计算成本更低的更好的解决方案。这在ImageNet-A上获得了名列前五的97.87%的准确率,也是第一次针对单个体系结构的。结论我希望本文能让你对这些比较重要的卷积组块有一个深刻的理解,并且能够认识到实现起来可能比想象的要容易。要进一步了解这些体系结构,请查看相关的论文。你会发现,一旦掌握了一篇论文的核心思想,就会更容易理解其余的部分了。另外,在实际的实现过程中通常将批量规范化添加到混合层中,并且随着激活函数应用的的地方会有所变化。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 4, 2018 · 2 min · jiezi

ES6 系列之模块加载方案

前言本篇我们重点介绍以下四种模块加载规范:AMDCMDCommonJSES6 模块最后再延伸讲下 Babel 的编译和 webpack 的打包原理。require.js在了解 AMD 规范之前,我们先来看看 require.js 的使用方式。项目目录为:* project/ * index.html * vender/ * main.js * require.js * add.js * square.js * multiply.jsindex.html 的内容如下:<!DOCTYPE html><html> <head> <title>require.js</title> </head> <body> <h1>Content</h1> <script data-main=“vender/main” src=“vender/require.js”></script> </body></html>data-main=“vender/main” 表示主模块是 vender 下的 main.js。main.js 的配置如下:// main.jsrequire([’./add’, ‘./square’], function(addModule, squareModule) { console.log(addModule.add(1, 1)) console.log(squareModule.square(3))});require 的第一个参数表示依赖的模块的路径,第二个参数表示此模块的内容。由此可以看出,主模块依赖 add 模块和 square 模块。我们看下 add 模块即 add.js 的内容:// add.jsdefine(function() { console.log(‘加载了 add 模块’); var add = function(x, y) { return x + y; }; return { add: add };});requirejs 为全局添加了 define 函数,你只要按照这种约定的方式书写这个模块即可。那如果依赖的模块又依赖了其他模块呢?我们来看看主模块依赖的 square 模块, square 模块的作用是求出一个数字的平方,比如输入 3 就返回 9,该模块依赖一个乘法模块,该乘法模块即 multiply.js 的代码如下:// multiply.jsdefine(function() { console.log(‘加载了 multiply 模块’) var multiply = function(x, y) { return x * y; }; return { multiply: multiply };});而 square 模块就要用到 multiply 模块,其实写法跟 main.js 添加依赖模块一样:// square.jsdefine([’./multiply’], function(multiplyModule) { console.log(‘加载了 square 模块’) return { square: function(num) { return multiplyModule.multiply(num, num) } };});require.js 会自动分析依赖关系,将需要加载的模块正确加载。requirejs 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/requirejs而如果我们在浏览器中打开 index.html,打印的顺序为:加载了 add 模块加载了 multiply 模块加载了 square 模块29AMD在上节,我们说了这样一句话:requirejs 为全局添加了 define 函数,你只要按照这种约定的方式书写这个模块即可。那这个约定的书写方式是指什么呢?指的便是 The Asynchronous Module Definition (AMD) 规范。所以其实 AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。你去看 AMD 规范) 的内容,其主要内容就是定义了 define 函数该如何书写,只要你按照这个规范书写模块和依赖,require.js 就能正确的进行解析。sea.js在国内,经常与 AMD 被一起提起的还有 CMD,CMD 又是什么呢?我们从 sea.js 的使用开始说起。文件目录与 requirejs 项目目录相同:* project/ * index.html * vender/ * main.js * require.js * add.js * square.js * multiply.jsindex.html 的内容如下:<!DOCTYPE html><html><head> <title>sea.js</title></head><body> <h1>Content</h1> <script src=“vender/sea.js”></script> <script> // 在页面中加载主模块 seajs.use("./vender/main"); </script></body></html>main.js 的内容如下:// main.jsdefine(function(require, exports, module) { var addModule = require(’./add’); console.log(addModule.add(1, 1)) var squareModule = require(’./square’); console.log(squareModule.square(3))});add.js 的内容如下:// add.jsdefine(function(require, exports, module) { console.log(‘加载了 add 模块’) var add = function(x, y) { return x + y; }; module.exports = { add: add };});square.js 的内容如下:define(function(require, exports, module) { console.log(‘加载了 square 模块’) var multiplyModule = require(’./multiply’); module.exports = { square: function(num) { return multiplyModule.multiply(num, num) } };});multiply.js 的内容如下:define(function(require, exports, module) { console.log(‘加载了 multiply 模块’) var multiply = function(x, y) { return x * y; }; module.exports = { multiply: multiply };});跟第一个例子是同样的依赖结构,即 main 依赖 add 和 square,square 又依赖 multiply。seajs 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/seajs而如果我们在浏览器中打开 index.html,打印的顺序为:加载了 add 模块2加载了 square 模块加载了 multiply 模块9CMD与 AMD 一样,CMD 其实就是 SeaJS 在推广过程中对模块定义的规范化产出。你去看 CMD 规范的内容,主要内容就是描述该如何定义模块,如何引入模块,如何导出模块,只要你按照这个规范书写代码,sea.js 就能正确的进行解析。AMD 与 CMD 的区别从 sea.js 和 require.js 的例子可以看出:1.CMD 推崇依赖就近,AMD 推崇依赖前置。看两个项目中的 main.js:// require.js 例子中的 main.js// 依赖必须一开始就写好require([’./add’, ‘./square’], function(addModule, squareModule) { console.log(addModule.add(1, 1)) console.log(squareModule.square(3))});// sea.js 例子中的 main.jsdefine(function(require, exports, module) { var addModule = require(’./add’); console.log(addModule.add(1, 1)) // 依赖可以就近书写 var squareModule = require(’./square’); console.log(squareModule.square(3))});2.对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。看两个项目中的打印顺序:// require.js加载了 add 模块加载了 multiply 模块加载了 square 模块29// sea.js加载了 add 模块2加载了 square 模块加载了 multiply 模块9AMD 是将需要使用的模块先加载完再执行代码,而 CMD 是在 require 的时候才去加载模块文件,加载完再接着执行。感谢感谢 require.js 和 sea.js 在推动 JavaScript 模块化发展方面做出的贡献。CommonJSAMD 和 CMD 都是用于浏览器端的模块规范,而在服务器端比如 node,采用的则是 CommonJS 规范。导出模块的方式:var add = function(x, y) { return x + y;};module.exports.add = add;引入模块的方式:var add = require(’./add.js’);console.log(add.add(1, 1));我们将之前的例子改成 CommonJS 规范:// main.jsvar add = require(’./add.js’);console.log(add.add(1, 1))var square = require(’./square.js’);console.log(square.square(3));// add.jsconsole.log(‘加载了 add 模块’)var add = function(x, y) { return x + y;};module.exports.add = add;// multiply.jsconsole.log(‘加载了 multiply 模块’)var multiply = function(x, y) { return x * y;};module.exports.multiply = multiply;// square.jsconsole.log(‘加载了 square 模块’)var multiply = require(’./multiply.js’);var square = function(num) { return multiply.multiply(num, num);};module.exports.square = square;CommonJS 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/commonJS如果我们执行 node main.js,打印的顺序为:加载了 add 模块2加载了 square 模块加载了 multiply 模块9跟 sea.js 的执行结果一致,也是在 require 的时候才去加载模块文件,加载完再接着执行。CommonJS 与 AMD引用阮一峰老师的《JavaScript 标准参考教程(alpha)》:CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用 AMD 规范。ES6ECMAScript2015 规定了新的模块加载方案。导出模块的方式:var firstName = ‘Michael’;var lastName = ‘Jackson’;var year = 1958;export {firstName, lastName, year};引入模块的方式:import {firstName, lastName, year} from ‘./profile’;我们再将上面的例子改成 ES6 规范:目录结构与 requirejs 和 seajs 目录结构一致。<!DOCTYPE html><html> <head> <title>ES6</title> </head> <body> <h1>Content</h1> <script src=“vender/main.js” type=“module”></script> </body></html>注意!浏览器加载 ES6 模块,也使用 <script> 标签,但是要加入 type=“module” 属性。// main.jsimport {add} from ‘./add.js’;console.log(add(1, 1))import {square} from ‘./square.js’;console.log(square(3));// add.jsconsole.log(‘加载了 add 模块’)var add = function(x, y) { return x + y;};export {add}// multiply.jsconsole.log(‘加载了 multiply 模块’)var multiply = function(x, y) { return x * y;};export {multiply}// square.jsconsole.log(‘加载了 square 模块’)import {multiply} from ‘./multiply.js’;var square = function(num) { return multiply(num, num);};export {square}ES6-Module 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/ES6值得注意的,在 Chrome 中,如果直接打开,会报跨域错误,必须开启服务器,保证文件同源才可以有效果。为了验证这个效果你可以:cnpm install http-server -g然后进入该目录,执行http-server在浏览器打开 http://localhost:8080/ 即可查看效果。打印的顺序为:加载了 add 模块加载了 multiply 模块加载了 square 模块29跟 require.js 的执行结果是一致的,也就是将需要使用的模块先加载完再执行代码。ES6 与 CommonJS引用阮一峰老师的 《ECMAScript 6 入门》:它们有两个重大差异。CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。第二个差异可以从两个项目的打印结果看出,导致这种差别的原因是:因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。重点解释第一个差异。CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。举个例子:// 输出模块 counter.jsvar counter = 3;function incCounter() { counter++;}module.exports = { counter: counter, incCounter: incCounter,};// 引入模块 main.jsvar mod = require(’./counter’);console.log(mod.counter); // 3mod.incCounter();console.log(mod.counter); // 3counter.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。但是如果修改 counter 为一个引用类型的话:// 输出模块 counter.jsvar counter = { value: 3};function incCounter() { counter.value++;}module.exports = { counter: counter, incCounter: incCounter,};// 引入模块 main.jsvar mod = require(’./counter.js’);console.log(mod.counter.value); // 3mod.incCounter();console.log(mod.counter.value); // 4value 是会发生改变的。不过也可以说这是 “值的拷贝”,只是对于引用类型而言,值指的其实是引用。而如果我们将这个例子改成 ES6:// counter.jsexport let counter = 3;export function incCounter() { counter++;}// main.jsimport { counter, incCounter } from ‘./counter’;console.log(counter); // 3incCounter();console.log(counter); // 4这是因为ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。Babel鉴于浏览器支持度的问题,如果要使用 ES6 的语法,一般都会借助 Babel,可对于 import 和 export 而言,只借助 Babel 就可以吗?让我们看看 Babel 是怎么编译 import 和 export 语法的。// ES6var firstName = ‘Michael’;var lastName = ‘Jackson’;var year = 1958;export {firstName, lastName, year};// Babel 编译后’use strict’;Object.defineProperty(exports, “__esModule”, { value: true});var firstName = ‘Michael’;var lastName = ‘Jackson’;var year = 1958;exports.firstName = firstName;exports.lastName = lastName;exports.year = year;是不是感觉有那么一点奇怪?编译后的语法更像是 CommonJS 规范,再看 import 的编译结果:// ES6import {firstName, lastName, year} from ‘./profile’;// Babel 编译后’use strict’;var _profile = require(’./profile’);你会发现 Babel 只是把 ES6 模块语法转为 CommonJS 模块语法,然而浏览器是不支持这种模块语法的,所以直接跑在浏览器会报错的,如果想要在浏览器中运行,还是需要使用打包工具将代码打包。webpackBabel 将 ES6 模块转为 CommonJS 后, webpack 又是怎么做的打包的呢?它该如何将这些文件打包在一起,从而能保证正确的处理依赖,以及能在浏览器中运行呢?首先为什么浏览器中不支持 CommonJS 语法呢?这是因为浏览器环境中并没有 module、 exports、 require 等环境变量。换句话说,webpack 打包后的文件之所以在浏览器中能运行,就是靠模拟了这些变量的行为。那怎么模拟呢?我们以 CommonJS 项目中的 square.js 为例,它依赖了 multiply 模块:console.log(‘加载了 square 模块’)var multiply = require(’./multiply.js’);var square = function(num) { return multiply.multiply(num, num);};module.exports.square = square;webpack 会将其包裹一层,注入这些变量:function(module, exports, require) { console.log(‘加载了 square 模块’); var multiply = require("./multiply"); module.exports = { square: function(num) { return multiply.multiply(num, num); } };}那 webpack 又会将 CommonJS 项目的代码打包成什么样呢?我写了一个精简的例子,你可以直接复制到浏览器中查看效果:// 自执行函数(function(modules) { // 用于储存已经加载过的模块 var installedModules = {}; function require(moduleName) { if (installedModules[moduleName]) { return installedModules[moduleName].exports; } var module = installedModules[moduleName] = { exports: {} }; modules[moduleName](module, module.exports, require); return module.exports; } // 加载主模块 return require(“main”);})({ “main”: function(module, exports, require) { var addModule = require("./add"); console.log(addModule.add(1, 1)) var squareModule = require("./square"); console.log(squareModule.square(3)); }, “./add”: function(module, exports, require) { console.log(‘加载了 add 模块’); module.exports = { add: function(x, y) { return x + y; } }; }, “./square”: function(module, exports, require) { console.log(‘加载了 square 模块’); var multiply = require("./multiply"); module.exports = { square: function(num) { return multiply.multiply(num, num); } }; }, “./multiply”: function(module, exports, require) { console.log(‘加载了 multiply 模块’); module.exports = { multiply: function(x, y) { return x * y; } }; }})最终的执行结果为:加载了 add 模块2加载了 square 模块加载了 multiply 模块9参考《JavaScript 标准参考教程(alpha)》《ECMAScript6 入门》手写一个CommonJS打包工具(一)ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

November 13, 2018 · 5 min · jiezi