在上一篇文章裏,已經對v2版本的koa中間件原理作了逐行分析,講清楚了它的流程控制和異步方案。javascript
可是,仍然有大量的基於koa的項目、框架、庫在基於v1版本的koa在工做,而它的中間件是Generator函數,其運行機制與v2版本的koa中間件有比較大的不一樣。前端
所以,有必要解釋清楚v1版本的koa中間件原理,做爲對上一篇文章的補充,但願能對那些仍然在項目中使用v1版本koa的同行同窗有所幫助。java
PS:本文基於v1.6.2的koa源碼,結構圖以下:node
本段內容與上一篇文章大體相同,已經看過該文章的話,能夠跳過這一節git
與上一篇文章同樣,先從一個簡單的Demo開始,來看Koa的使用方式。es6
const Koa = require('koa');
const app = new Koa();
複製代碼
Koa變量指向是什麼呢?咱們知道:github
require在查找第三方模塊時,會查找該模塊下package.json文件的main字段。json
查看koa倉庫目錄下下package.json文件,能夠看到模塊暴露的出口是lib目錄下的application.js文件api
{
"main": "lib/application.js",
}
複製代碼
在lib/application
文件中,能夠看到其模塊出口以下:數組
var app = Application.prototype;
module.exports = Application;
function Application(){}
複製代碼
好,如今來給咱們的Demo添加中間件
const Koa = require('koa');
const app = new Koa();
const one = function* (next){
console.log('1-Start');
const t = yield next;
console.log('1-End');
}
const final = function* (next) {
console.log('final-Start');
this.body = { text: 'Hello World' };
console.log('final-End');
}
app.use(one);
app.use(final);
app.listen(3005);
複製代碼
以上這段代碼中,ctx.body 如何實現並非本文的重點,只要知道它的做用是設置響應體的數據,就能夠了。
可是要弄清楚的關鍵有亮點
先來看use函數
app.use = function(fn){
// 省略與中間件機制無關的代碼
this.middleware.push(fn);
return this;
};
複製代碼
根據上文提到的var app = Application.prototype;
語句,能夠知道use方法掛載到了Application.prototype上,所以每一個實例均可以調用use。
use方法作的事情也很簡單,把傳入的函數,存入到實例的middleware數組當中。
再來看listen方法:
app.listen = function(){
// 省略無關代碼
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
複製代碼
若是曾使用過Node的[http]模塊建立過簡單的服務器應用的話,就會知道http.createServer的參數一個函數,函數的參數分別是請求對象request和相應對象response,即形如如下結構:
(req, res) => {
// Do Sth.
res.end('Hello World')
}
複製代碼
所以this.callback函數執行所返回的結果,也必定是這樣一個結構,咱們來看這個callback函數
app.callback = function(){
// 省略一些校驗代碼
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
var self = this;
// 省略一些錯誤處理代碼,與中間件機制不要緊
return function handleRequest(req, res){
var ctx = self.createContext(req, res);
self.handleRequest(ctx, fn);
}
};
複製代碼
看到返回的handleRequest函數的結構了嗎?它會被傳遞給http.createServer,所以在每次接收到請求時,都會執行該函數。
那好,再來看self.handleRequest函數。
app.handleRequest = function(ctx, fnMiddleware){
ctx.res.statusCode = 404;
onFinished(ctx.res, ctx.onerror);
fnMiddleware.call(ctx).then(function handleResponse() {
respond.call(ctx);
}).catch(ctx.onerror);
};
複製代碼
這個函數接受一個上下文ctx對象做爲第一個參數,這裏只要記住它是一個對象就能夠,後面會被傳遞給每一箇中間件。它的第二個參數,是一個名爲fnMiddleware的函數,它是什麼呢?回過頭去看app.handleRequest
被執行的地方。
self.handleRequest(ctx, fn);
複製代碼
這裏的fn又是什麼?再去找fn的定義,發現
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
複製代碼
原來fn是this.middleware通過一些處理後獲得的函數,這些工做具體作了什麼,後文會說。這裏只要先記住fn是一個組合後的函數,執行了它,那麼一系列中間件就會依次執行。
如今fn清楚了,也就是self.handleRequest
函數的第二個參數就清楚了,接着剛剛沒有說完的話題,看看這個self.handleRequest
作了什麼事。
function(ctx, fnMiddleware){
// 忽略其餘非關鍵代碼
fnMiddleware.call(ctx).then(function handleResponse() {
respond.call(ctx);
}).catch(ctx.onerror);
};
複製代碼
也很簡單,以上下文ctx對象做爲this,去調用fnMiddleware函數其返回的結果是一個Promise,而且使用了該Promise的then/catch方法,添加了兩個流程:
respond.call(ctx)
.catch(ctx.onerror)
因此,總結一下:
好,v1版本的響應機制已經介紹完畢,各位也差很少能知道中間件是在何時執行的了。若是有不明白的地方,能夠先看完上一篇講v2版本Koa原理的文章(傳送門)。
與以前基於v2版本的koa相比,v1版本的koa,在中間件原理上有個重大的不一樣之處,即
v1版本的koa的執行工做,是交給Co庫完成的,而v2則是本身完成的。
這話什麼意思???這與Koa使用Generator函數做爲中間件函數有關。
因此,在正式開始介紹v1版本koa的中間件原理以前,有一些前置知識要先解釋清楚。
(已經熟悉Generator和Co庫原理的各位,能夠直接跳過介紹這兩章)
Generator 是一種特殊的函數,它的執行結果是一個Iterator,便可迭代對象。
那Iterator又是什麼呢? 能夠這麼說,任何一個對象,只要符合迭代器協議,就能夠被認爲是一個Iterator。通俗一點說,該協議所約定的對象,有兩個關鍵點:
示例以下,下面的it對象,由於符合迭代器協議,就是一個Iterator(儘管它不是由Generator返回的)
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
複製代碼
那麼Iterator有什麼用呢?它本來的做用,是用於自定義對象的迭代行爲。
而爲何又要定義對象的迭代行爲呢?按照阮一峯老師在Iterator和for...of循環裏的說法
遍歷器(Iterator)就是這樣一種機制。它是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)。
對此個人理解是,「定義對象的迭代行爲」的意義,在於「爲各類不一樣的數據結構提供統一的訪問機制」,即把對於迭代的描述工做交給了數據結構自身,開發者只需調用單個API便可(即for...of...),這將會節約開發者記憶API的成本。正所謂「對象千萬種,規範第一條;迭代不規範,開發淚兩行」...
而若是要定義對象的迭代行爲,就要在它的[Symbol.iterator]屬性上,定義一個函數,而且返回一個符合迭代器協議的對象(即Iterator)。(參考:可迭代協議)。
好比,咱們把上面的代碼改一改,就可使用for...of...來執行迭代了。
const arr = ['a', 'b'];
arr[Symbol.iterator] = function () {
let i = 0;
return {
next: function () {
const done = i >= arr.length;
if (!done) {
console.log('Not Done!')
return { value: arr[i++], done };
} else {
return { done };
}
}
}
}
for(let i of arr){}; // 輸出兩次Not Done
複製代碼
可是從上面的代碼能夠看到,什麼時候完成迭代(控制done的值),每次迭代返回value值是什麼,都交給了next函數來維護,須要寫的維護代碼較多。
這時候咱們能夠回到Generator函數了,由於Generator函數就是爲此而生的。它爲這種維護工做,提供了簡化的寫法,只要經過yield命令給出返回值就能夠了,最後yield所有結束的時候。
arr[Symbol.iterator] = function* () {
for(let i = 0; i < arr.length; i++){
console.log(`輸出文本:yield arr[${i}]`)
yield arr[i]
}
return ;
}
for(let i of arr){};
// 輸出文本:yield arr[0]
// 輸出文本:yield arr[1]
複製代碼
因此,我的認爲,Generator函數是一種語法糖,它描述的是若干個代碼塊的分段執行順序。
既然Generator有分段執行的功能,就能夠處理異步問題了。
不過,值得注意的是,yield並非真正的異步邏輯,它只是把yield後面的值,在執行next方法的時候返回出去而已。好比當yield 後面的表達式返回的是一個Promise的時候:
function* gen1(){
yield 1;
yield new Promise(resolve => setTimeout(() => resolve(1), 1000));
yield 2;
}
const iterator = gen1();
複製代碼
開始跑iterator的next方法
iterator.next(); // {value: 1, done: false};
iterator.next(); // {value: Promise, done: false};
iterator.next(); // {value: 2, done: false};
複製代碼
看到了嗎?第二句next方法只是返回了Promise對象而已,根本沒有等着它的then回調執行。
因此,要想用Generator來作異步操做,其基本思路只能是以下:執行第一次next -> 等待promise 完成 -> 執行第二次next...舉個例子:
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
// value是個Promise,下一次g.next執行要交給promise的then回調
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
複製代碼
可是,這顯然有問題:gen
函數返回的Iterator,必須手動執行才能進行到下一個yield,不會自動執行。若是異步方案僅僅是如此,那開發者還不如本身寫Promise鏈呢。
這就是co的做用了!它會:
一般來講,咱們使用co庫,會像是如今這樣
const co = require('co');
const mockTimeoutPromise = (data, timeout = 1000) => new Promise(resolve => {
setTimeout(() => {
console.log(data);
resolve(data)
}, timeout);
});
co(function* () {
yield mockTimeoutPromise(1);
const result = yield { name: 'co' };
return result;
})
.then(value => {
console.log(value);
})
複製代碼
接下來咱們就用這份源代碼,來分析co庫究竟是怎麼作的
能夠看到,co函數接受了一個Generator函數做爲參數。
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
return new Promise(function(resolve, reject) {
});
}
複製代碼
能夠看到,co函數的返回值是一個Promise,它對應的是當次co函數內包裹的整個流程,一旦該Promise被resolved,就意味着co函數所接受的入參函數,其內部流程已經徹底執行完畢。
好,接下來來Promise構造函數內部的內容。
首先是一段執行傳入的Generator函數的代碼,得到Iterator
// 在某些狀況下,傳入的gen參數自己就是一個Iterator對象,不是Generator函數,所以會作類型檢查
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// 確保得到的結果,是一個Iterator
if (!gen || typeof gen.next !== 'function') return resolve(gen);
複製代碼
接着就是開始執行onFulfilled函數。
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
複製代碼
在初次執行時,入參res的值爲空,以剛剛提供的Demo來看,執行gen.next(res)獲得的ret值,應該是這樣的結構:
{
done: false, value: Promise
}
複製代碼
也就是說,其value值,是Demo代碼中mockTimeoutPromise
函數的執行結果。
接着,就把ret值傳入next函數
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
複製代碼
分析這段代碼,其執行邏輯爲:
toPromise
函數將當前的迭代值,變成Promise類型在初次流程中,因爲是第一句yield,因此ret.done爲false,開始執行toPromise邏輯,接着來看toPromise函數
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
複製代碼
這段代碼的主要功能是將各類類型的值,包裹成了Promise值,其中各個轉換函數的邏輯在這裏不是重點,所以不加詳細闡述。
所以,若是咱們寫出這樣的代碼
yield 2;
yield 3;
複製代碼
toPromise就會直接返回這些被yield的常量值,而不轉化爲promise類型。這時候咱們再回到toPromise被執行的位置,即next函數內部,就會發現沒法經過value && isPromise(value)
校驗,就會走onRejected報錯。
這也是爲何,咱們有時候會看見這樣的錯誤
You may only yield a function, promise, generator, array, or object, but the following object was passed: "2"
就是由於咱們yield了一個非yieldables的值。
回到toPromise函數,其中有兩個轉換邏輯很是值得一提:
一是對於Generator的識別:若是識別爲是Generator函數或者Generator函數所返回的Iterator,就會再次用co包裹一層,返回一個Promise。
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
複製代碼
其中:
isGeneratorFunction
函數識別參數是否爲Generator函數isGenerator
函數識別參數是否被Generator函數返回的Iterator私人吐槽時間:isGenerator函數也許更名爲isIteratorOfGenerator也許更標準一點。
二則是objectToPromise
,一般咱們在使用yield的時候,會看見這樣的邏輯:
const { result1, result2 } = yield {
result1: Promise1,
result2: Promise2
}
複製代碼
這是怎麼作到的呢?實際上是依靠objectToPromise
函數,它的代碼邏輯以下:
function objectToPromise(obj){
var results = new obj.constructor();
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
if (promise && isPromise(promise)) defer(promise, key);
else results[key] = obj[key];
}
return Promise.all(promises).then(function () {
return results;
});
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}
複製代碼
它主要是作了這麼幾件事:
var results = new obj.constructor();
var keys = Object.keys(obj);
複製代碼
var promises = [];
複製代碼
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
}
複製代碼
if (promise && isPromise(promise)) defer(promise, key);
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
複製代碼
else results[key] = obj[key];
複製代碼
return Promise.all(promises).then(function () {
return results;
});
複製代碼
好,回到剛剛的toPromise函數,因爲Demo代碼裏的第一句是
yield mockTimeoutPromise(1);
複製代碼
因此toPromise函數會執行第二句
if (isPromise(obj)) return obj;
複製代碼
所以能夠經過next函數的isPromise校驗
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
複製代碼
好,第一輪循環至此結束,再也沒有其餘代碼要執行了。
接下來,由於mockTimeoutPromise
函數默認的超時值timeout爲1000,因此1秒以後,上面的value(是一個Promise)被resolve,開始繼續執行onFulfilled函數
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
複製代碼
此時,ret的返回結構爲
{
done: false,
value: { name: 'co' };
}
複製代碼
所以會繼續執行 next函數 -> toPromise函數 -> objectToPromise函數。所以,toPromise函數會獲得這樣一個結果
Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: Object
複製代碼
所以也能夠經過next函數的isPromise
校驗,至此第二輪yield結束。
接下來
ret = gen.next(res);
複製代碼
獲得的ret.done值爲true,因此第三次執行next函數,就會走
if (ret.done) return resolve(ret.value);
複製代碼
至此,整個流程結束。
好,Co的工做流程已經大體理清楚,在此作個小結:
-> 包裹一層Promise,用以判斷當前整個異步流程終結
-> 執行傳入的Generator函數,得到Iterator
-> 執行Iterator對象的next方法,得到當前值value/done
-> 若done爲false,包裝value值爲Promise,在該Promise的then/catch回調中,執行下一次next
-> 若done爲true,整個流程已終結,將外層Promise給resolved。
若是暫時沒有理解前置知識相關章節的代碼,能夠直接看下面的幾點總結(看懂的能夠跳過)
Generator函數:
co函數庫
好了,有了這些基礎知識,咱們能夠開始看中間件的執行了。
在第一節的「請求響應機制」部分提到,fnMiddlewawre是一個把全部中間件組合後獲得的函數,它會在每次收到請求的時候執行,來看它的組合代碼
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
複製代碼
查遍koa v1.6.2的源碼,發現experimental屬性並無賦值語句,因此咱們能夠認爲,只要你不寫這樣的代碼
const app = new Koa();
app.experimental = true;
複製代碼
那麼,experimental始終會是undefined
,也就是一個falsy的值。它必定會走的是
co.wrap(compose(this.middleware));
複製代碼
因而,咱們就有兩件事情要說清楚:
先說co-wrap,由於它很是簡單
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
複製代碼
它接受一個函數fn
,返回另外一個函數createPromise
。
從上面的代碼能夠知道,返回的createPromise
,就是fnMiddleware
函數,而fnMiddleware
的執行語句是這樣的:
fnMiddleware.call(ctx)
複製代碼
因此createPromise
在獲得執行時,內部的this,就是上下文ctx對象。
而這createPromise
函數的職責也很簡單,就是把傳入的fn參數,用co庫來執行了一遍。
因此,咱們來看compose函數作了什麼。
查找頂部引入模塊的語句,能夠看到
var compose = require('koa-compose');
複製代碼
好,咱們來看koa-compose的模塊,究竟是什麼功能
PS:koa-compose基於2.5.1;
module.exports = compose;
// 空函數
function *noop(){}
function compose(middleware){
// 這個被返回的函數,就是傳給co-wrap的fn參數
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
}
}
複製代碼
咱們仍是用一個Demo來看:
const Koa = require('koa');
const app = new Koa();
const one = function* (next){
console.log('1-Start');
const r = yield Promise.resolve(1);
const t = yield next;
console.log('1-End');
}
const final = function* (next) {
console.log('final-Start');
this.body = { text: 'Hello World' };
console.log('final-End');
}
app.use(one);
app.use(final);
app.listen(3005);
複製代碼
所以,middleware獲得的結果是[one, final]
,它們都是Generator函數,所以,執行下列語句的時候
// 因爲fnMiddleware.call(ctx)語句,未傳入第二個參數,所以初次調用時候next爲空,變成noop函數
if (!next) next = noop();
var i = middleware.length;
// 第一次 i 爲 2
var i = middleware.length;
// i--返回值爲2,i--以後i變爲1
while (i--) {
next = middleware[i].call(this, next);
}
複製代碼
第一次執行時,middleware[i]便是middleware[1],即final函數。此時,入參next爲noop函數,返回的next,指向final中間件執行後所返回的Iterator對象。
而下一次循環發生時
// i--返回值爲1,i--以後i變爲0
while (i--) {
next = middleware[i].call(this, next);
}
複製代碼
此時middleware[i].call(this, next);
,入參next,是final中間件返回的Iterator對象,即one中間件函數中的next參數
// 這個next參數是final中間件的Iterator噢
const one = function* (next){
console.log('1-Start');
const r = yield Promise.resolve(1);
const t = yield next;
console.log('1-End');
}
複製代碼
而返回的next則是one中間件函數執行所返回的Iterator。
再下一次循環發生時,此時循環不會發生
// i--返回值爲0,i--以後i變爲-1,不執行循環。
while (i--) {
next = middleware[i].call(this, next);
}
// 開始走return邏輯
return yield *next;
複製代碼
此時next即爲one中間件函數執行所返回的Iterator。
因此,koa-compose完成的工做,主要在於經過函數的組合,實現了next參數,即迭代器對象Iterator的傳遞。
根據yield*關鍵字,yield *next至關於yield one中間件的代碼
yield (
console.log('1-Start');
const r = yield Promise.resolve(1);
const t = yield next;
console.log('1-End');
)
複製代碼
但爲何這裏要寫yield *next,直接yield next很差嗎?
我的認爲,因爲co函數會識別yield關鍵字後面的值的類型並轉化爲Promise,即依次執行 toPromise函數 -> if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
,所以使用yield*和yield,在執行流程上沒有本質的區別。
可是,爲何用yield_呢?個人理解是,因爲最外層的next幾乎能夠肯定是一個Iterator,因此直接使用yield _,能夠減小一層co函數的調用。
由以前的分析能夠知道,fnMiddleware,實際上至關於這樣的結構
co(function*(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
})
複製代碼
因此,第一步,yield *next會幫助咱們去等待next迭代器完成迭代,而這個next,就是one中間件的迭代器。而在one中間件裏,能夠看到第一步是
console.log('1-Start');
const r = yield Promise.resolve(1);
複製代碼
在co的自動執行流程中,會等着這個Promise完成,纔會進行下一個yield。
而下一步,在one中間件中,則是
const t = yield next;
複製代碼
前面提到,one中間件函數的入參next,是final中間件返回的Iterator
而co會識別到這個next是一個Iterator,進而在toPromise函數中走包裝Iterator爲Promise的邏輯
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
複製代碼
所以,在one中間件中執行yield next時,會等到final中間件徹底執行完畢後,再回過頭來執行下一個yield語句。固然,one中間件已經沒有下一個yield語句了,所以它自身對應的next對象,也就執行完畢了。
用一段僞代碼來描述,就是這樣的:
yield (
console.log('1-Start');
const r = yield Promise.resolve(1);
const t = yield (
console.log('final-Start');
this.body = { text: 'Hello World' };
console.log('final-End');
)
console.log('1-End');
)
複製代碼
能夠看到,這就是Koa所說的洋蔥圈模型:
若是咱們把one中間件的代碼改改
const one = function* (next) {
console.log('one-Start');
this.body = { text: 'Hello World' };
console.log('one-End');
}
複製代碼
至關於在one中間件裏,並無經過yield next語句,來等待下一個中間件,也就是final中間件的執行完畢。所以能夠說,next參數就是one中間件對於下一個中間件「是否執行」的控制權。
至此,v1版本koa的中間件執行機制已經所有介紹完畢。
與Koa v2相比,Koa v1的流程控制方案是一致的,都是把下一個中間件的執行權,經過傳參數的方式,交給了當前中間件。
但不一樣的是,Koa v2傳遞的是包裝後的中間件函數自己,因此「下一個中間件的執行工做」,是當前中間件函數本身完成的。而Koa v1,則只是傳遞了迭代器對象Iterator,中間件函數只是描述了執行流程,具體的執行工做是交給Co工具庫來完成的。
而Koa v1的異步邏輯,也是交給Co庫完成。它經過判斷迭代器執行next方法所返回的值的類型,並經過toPromise函數轉化成Promise類型的指,待該Promise被resolve時,再來下一次next方法執行的時機,進而實現了異步邏輯。
咱們是螞蟻保險體驗技術團隊,來自螞蟻金服保險事業羣(杭州/上海)。咱們是一個年輕的團隊(沒有歷史技術棧包袱),目前平均年齡92年(去除一個最高分8x年-團隊leader,去除一個最低分97年-實習小老弟)。咱們支持了阿里集團幾乎全部的保險業務。18年咱們產出的相互寶轟動保險界,19年咱們更有多個重量級項目籌備動員中。現伴隨着事業羣的高速發展,團隊也在迅速擴張,歡迎各位前端高手加入咱們~
咱們但願你是:技術上基礎紮實、某領域深刻(Node/互動營銷/數據可視化等);學習上善於沉澱、持續學習;性格上樂觀開朗、活潑外向。
若有興趣加入咱們,歡迎發送簡歷至郵箱:shuzhe.wsz@alipay.com
本文做者:螞蟻保險-體驗技術組-漸臻
掘金地址:DC大錘