Quantcast
Channel: CNode:Node.js专业中文社区
Viewing all 14821 articles
Browse latest View live

直播讲京东抢购的网络请求分析

$
0
0

今晚3.8日直播讲京东抢购的网络请求分析,有想看的小伙伴备注python数据加我微信lujqme我拉你入群。

或者对数据感兴趣的朋友们也欢迎呢~

另外北京 数据岗求职,谢谢大家


nuxtjs+express+vue2+vuex搭建的服务端渲染(SSR)个人网站项目

$
0
0

5se7en.com

nuxtjs+express+vue2.0+vuex搭建的服务端渲染个人网站项目.<br> github项目地址: https://github.com/se7en-1992/5se7en.com项目线上地址:https://5se7en.com/

注意事项

  • node>=v8.0.0+ (nuxt1.0.0以上版本的node版本号必须大于v8.0.0否则启动的时候会报错)<br>
  • 若要测试游戏登录请点击前往套马游戏注册一个账号

技术选型

这里先说两句题外话,谈一谈对前端开发产生了深远影响的两个时间点<br>

  • ajax的出现,促成了Web 2.0时代的来临
  • nodejs的出现,让前端能做的更多,让js不仅仅只是浏览器端的语言。 这里为什么要说这个呢,有些前端开发者会说node不是做后端的吗?我为什么要学nodejs呢?其实随着前端的发展,尤其是node出现,前端发展日新月异,各种自动化工具,框架层出不穷。很多都是依赖node。node不仅仅只是用来拿来写后端,可以这么说,当前时间如果你对node毫无知晓,也不去学的话,那么你已经被前端浪潮所覆盖了。
  • 本项目用的是node中使用最多的express前端web框架,官网的demo是这么形容的=> ExpressJS + Nuxt.js = ⚡️ 没错可以说是非常极速了,我的个人网站服务器是阿里云1核1g学生版机,网站除了第一次打开稍微慢点,后面可以说得上是光速了。比我以前使用的任何框架都要极速。
  • 下面再谈一下为什么要使用服务端渲染和选用nuxtjs?
    • 知乎上有个论题大家可以看一看为什么现在又流行服务端渲染html?,回答的人比较多,也比较杂,我这里就简单的总结一下
      • 服务端渲染,主要解决两个痛点 SEO优化(一些新闻资讯类的网站都需要做一些搜索引擎优化)和大型应用的首屏渲染(解决一些大型应用首页加载速度问题)
      • 其实这又要谈到历史了,一开始html就是后端渲染的,前端实际上就是在切图(CSS)和做特效(JS),所以所有程序员中前端工资最低,职位也最低。所以前后端的鄙视链就出现了。
      • nodejs 和前端 mvc 的兴起让前端变得复杂起来,前端发现翻身的机会,于是全力支持这两种技术,造成本不该做成 spa 的网站也成了 spa。慢慢地前后端分离运动从大公司开始兴起,目的就是前端脱离后端的指指点点,独立发展。(表面上是为了「代码分离」,实际上是为了「人员分离」,也就是「前后端分家」,前端不再附属于后端团队)
      • spa 之后发现 seo 问题很大,而且首屏渲染速度贼慢,但是自己选的路再难走也要走下去,于是用 nodejs 在服务端渲染这一条路被看成是一条出路
      • 简而言之就是前端一开始骚不起来,后来node和MVC/MVVM(Vue,React,Angular)的出现前端开始骚起来了,搞独立,把本应要做成服务端渲染的东西也做成了SPA,现在新技术又出来了,要及时发现错误,进行改正。前后端分离是趋势,既然都分开了,总不能还让后端去渲染,那咱们前端自己想办法做服务端渲染吧,于是服务端渲染框架也就出现了。
    • 为什么选用nuxtjs?
      • 一开始我用的服务端渲染是学习N-blog利用nodejs的express+ejs模版渲染做的,效果其实也还不错,里面的代码并没有完全的组件化,我做的项目还使用的jQuery,这多low啊,那我怎么能忍,我肯定要换个技术来玩。(这里没有贬低jQuery的意思,我觉得jQuery是个很不错的JavaScript库,曾经也可以说是一统前端了,包括现在,不会用jQuery的前端基本上没几个,但是怎么说呢,jQuery在慢慢沉寂,操作dom在现在对比下来并不是一个最优的选择了。)前端在不断发展,我们要做的就是选择最优。
      • 在vue官网中也对nuxtjs做了强力的推荐,再加上nuxtjs的github上express模版demo介绍ExpressJS + Nuxt.js =⚡看到这个我就选了这个框架了。没错就是他了.
      • nuxtjs结合vue2、Webpack、vue-loader、babel-loader、vuex、Vue-Meta
      • 不需要在配置繁琐的webpack配置,vue-loader自动生成路由,只需要在pages目录下创建文件就是自动生成对应的路由文件

开发环境

  • Node.js: ^8.9.4
  • express: ^4.16.2
  • nuxtjs: ^1.0.0-rc11
  • vue: ^2.5.3
npm install
npm run dev

使用浏览器打开 http://localhost:5757

友情提示

nuxt介绍

  • nuxt详细的入门教程这里不做详细的介绍,官方文档讲解的已经非常详细了。这里简单介绍一下项目目录作用

nuxt目录介绍

  • assets

    • 如果你的静态资源文件需要 Webpack 做构建编译处理,可以放到 assets 目录,否则可以放到 static 目录中去。
    • Nuxt 服务器启动的时候,该目录下的文件会映射至应用的根路径 / 下,像 robots.txt 或 sitemap.xml 这种类型的文件就很适合放到 static 目录中。
  • components

    • 组件目录 components 用于组织应用的 Vue.js 组件。Nuxt.js 不会扩展增强该目录下 Vue.js 组件,即这些组件不会像页面组件那样有 asyncData方法的特性。 简而言之此目录就是普通的vue组件目录。
  • layouts

    • 该目录名为Nuxt.js保留的,不可更改。
    • 你可以自定义合适自己网站的默认样式和错误样式
  • middleware

    • 中间件执行流程顺序:
      • nuxt.config.js
      • 匹配布局
      • 匹配页面
  • pages

    • 该目录名为Nuxt.js保留的,不可更改。
    • 页面目录 pages 用于组织应用的路由及视图。Nuxt.js 框架读取该目录下所有的 .vue 文件并自动生成对应的路由配置。
    • 此页面的.vue文件都具有asyncDatafech方法。
  • plugins

    • 插件目录 plugins 用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。
    • 我们可以将element-ui或者mint-ui以及其他更多的插件都可以放在plugins中使用
  • static

    • 该目录名为Nuxt.js保留的,不可更改。
    • 静态文件目录 static 用于存放应用的静态文件,此类文件不会被 Nuxt.js 调用 Webpack 进行构建编译处理。 服务器启动的时候,该目录下的文件会映射至应用的根路径 / 下。
  • store

    • 该目录名为Nuxt.js保留的,不可更改。
    • store 目录用于组织应用的 Vuex 状态树 文件。 Nuxt.js 框架集成了 Vuex 状态树 的相关功能配置,在 store 目录下创建一个 index.js 文件可激活这些配置。

nuxt配置介绍

  • config

    • 此目录并不是nuxt自身目录而是一些项目经验促使我添加此目录从来更加方便的去管理和使用在项目中所需要的变量。
    • NEWRELIC_KEY:newrelic的密钥,newrelic是服务器端性能监控的一款软件
    • TIMBER_KEY:timber的密钥,timber是一种云日志记录系统,简单的来说就是纪录线上的一些日志
    • SENTRY_PROJECT_ID/SENTRY_PUBLIC_KEY/SENTRY_PRIVATE_KEY:Sentry的项目id,公钥,私钥,Sentry是一个开源的实时错误报告工具
    • porductionProxy/developmentProxy: 是nuxt的axios模块代理请求的路径设置
    • 其实我在项目启动的时候还使用了pm2有express项目经验的人用过都说好,有日志记录状态监控等,真的很好用。
  • nuxt.config.js

  • .editorconfig

    • EditorConfig是一套用于统一代码格式的解决方案
  • .eslintrc.js

    • ESLint是一个应用广泛的 JavaScript 代码检查工具
  • gitignore

    • git提交忽略项配置文件
  • newrelic.js

    • newrelic配置文件
  • start.js

    • express启动入口文件,开发环境直接使用nuxt启动并没有走start.js,生产环境用pm2启动的该文件

14/50*100的值为何不是28?

$
0
0

13/50*100的值没问题,是26

如果你是面试官或者出题人,面试-两年-前端攻城狮,请出几道题呗

$
0
0

-两年-前端攻城狮,你会出什么样的题?别发链接哦…

  • 擅长 javascript

element-ui 的表格排序

$
0
0

element的表格排序怎么做? :default-sort = "{prop: ‘date’, order: ‘descending’}"做到的只是把原来的顺序调个个儿,可是原数据就是乱的。。。不会,求大神告知,感激不尽

MYSQL ORDER BY 使用 IF 以及使用 IN

$
0
0

MYSQL ORDER BY 使用 IF 以及使用 IN

  • 本文非原创,原文在这里有如下表数据

    +----+------+
    | id | type |
    +----+------+
    |  1 |    1 |
    |  2 |    1 |
    |  3 |    1 |
    |  4 |    2 |
    |  5 |    2 |
    |  6 |    3 |
    |  7 |    3 |
    |  8 |    4 |
    |  9 |    4 |
    | 10 |    4 |
    +----+------+
    

使用 IF 语句

想要 type=3 的行排在前面,type为其他值的排在后面,可以这样写SQL:

	SELECT * FROM test ORDER BY IF(type=3,0,1);

结果如下:

	+----+------+
	| id | type |
	+----+------+
	|  6 |    3 |
	|  7 |    3 |
	|  1 |    1 |
	|  2 |    1 |
	|  3 |    1 |
	|  4 |    2 |
	|  5 |    2 |
	|  8 |    4 |
	|  9 |    4 |
	| 10 |    4 |
	+----+------+

解释:

IF(type=3,0,1)的意思是,对 type 附加一个一个隐藏属性,这个隐藏属性,可以是0或者1,也就是对 type进行排序的时候,优先判断type是不是等于3,如果是,返回0,不是,返回1。然后对type隐藏属性进行排序,也就是对0和1进行排序。

也就是说,可以IF语句,看成一个独立的column,然后进行排序,你可以在IF语句后添加排序条件ASC或者DESC,当然,默认的排序是ASC,可以不写,上面的例子,加入你想让type=3的数据排在后面,就可以IF语句后面添加DESC了,如下:

	SELECT * FROM test IF(type=3,0,1) DESC;

结果如下:

	+----+------+
	| id | type |
	+----+------+
	|  1 |    1 |
	|  2 |    1 |
	|  3 |    1 |
	|  4 |    2 |
	|  5 |    2 |
	|  8 |    4 |
	|  9 |    4 |
	| 10 |    4 |
	|  6 |    3 |
	|  7 |    3 |
	+----+------+

另外,你在进行隐藏属性优先排序的同时,对于剩下的排序,你也可以另外进行ASC或者DESC的排序

使用 IN 语句

上面的例子是满足单个条件,返回0 或者 1,如果需要用到一个范围呢?比如想让 type =2或者type=3的行排在前面呢?

可以使用 IN 语句

	SELECT * FROM ORDER BY type IN(2,3) DESC

结果如下

	+----+------+
	| id | type |
	+----+------+
	|  4 |    2 |
	|  5 |    2 |
	|  6 |    3 |
	|  7 |    3 |
	|  1 |    1 |
	|  2 |    1 |
	|  3 |    1 |
	|  8 |    4 |
	|  9 |    4 |
	| 10 |    4 |
	+----+------+

上面 type IN(2,3) DESC, type IN 语句进行判断,如果type的值在(2,3)里面,返回1,否则返回0,所以,满足条件的数据,因为返回值是1,进行DESC排序的时候,就被放在在最后。

其他情况类推

child_process和IPC探究(标题党,其实并没有IPC)

$
0
0

在以下几种种情况,nodejs需要新开子进程来进行相关操作

  • 长时间耗费CPU的操作,这个大家都懂,防止进程卡在一处以致后续的请求得不到响应。
  • 执行外部程序,如targcc
  • 提高处理效率,某些任务可以分解成多个并行的小任务,然后再汇总一起。

nodejs中使用child_process模块来对子进程进行生成、管理和通讯。网上关于这一块的介绍也不少,但是其中的细节感觉还是语焉不详,这里我们主要讨论3个问题(基于Linux平台):

  • spawnexecexecFilefork的区别
  • 底层如何生存子进程
  • 父进程和子进程之间的IPC通讯究竟是怎样

spawnexecexecFilefork

它们之间的调用关系如下

exec 
  │
execFile         fork
  │               │     
  └─────spawn─────┘          用户层面
          │
----------│------------------------
          │
          │                  nodejs内部
          │                   
     spawn(位于lib/internal/child_process.js)

从上图可易知:

  • 在用户层面,其它3个函数最终都是调用child_process.spawn
  • exec调用了execFile,因此它们的性态应该是一样的(缓存了IO)

接下来我们依次讨论一下这几个函数。

spawn

spawn的用法文档上已经写得很清晰,我们这里讨论值得关注的地方

关于子进程的stdio(标准输入输出)

通过修改options.stdio,可以将子进程的stdio可以绑定到不同的地方。options.stdio可以接受两种类型的值:字符串或者数组

  • options.stdio的值是字符串时,它有以下几种取值
    • pipe: 相当于[“pipe”,“pipe”,“pipe”],子进程的stdio与父进程的stdio通过管道连接起来,
    • ignore: 相当于[“ignore”,“ignore”,“ignore”],子进程的stdio绑定到/dev/null,丢弃数据的输入输出
    • inherit: 继承于父进程的相关stdio、即等同于[process.stdin, process.stdout, process.stderr]或者[0,1,2],此时父子进程的stdio都是绑定到同一个地方。
  • options.stdio的值是数组的时候,前三个元素分别代表stdin stdout stderr。如果数组的元素大于3,则会在父子进程之间建立 额外的通讯通道,它们的值可以是下面的其中之一

    • pipe额外的通讯通道通过管道通讯。管道的两端分别连接着父子进程,在父进程这边可以通过subprocess.stdio[n](n=0、1、2)或者subprocess.stdin, subprocess.stdout, subprocess.stderr来引用管道的一端,而子进程则可以通过process.stdin, process.stdout, process.stderr来引用另外一端,详情可以见面的例子。
    • ipc额外的通讯通道通过ipc channel通讯
    • ignore:绑定到/dev/null,即丢弃数据的输入输出
    • Stream对象:额外的通讯通道通过nodejs中Stream对象通讯,对象底层的文件描述符代表一个文件例如socket,tty、本地文件等。
    • 正整数:和Stream相似。
    • nullundefined:对于前3个元素,它们会被设为pipe,对于剩下的元素会被设置ignore

以下的例子,它将stdio绑定到不同的地方

"use strict";
const child_process = require("child_process");
const script = (function(data) {
  console.log(data);
}).toString();
child_process.spawn("node", ["-e", `( ${script}("inherit,这一般个会写到控制台") )`], {
  //子进程的stdio继承父进程的stdio,即控制台,因此会输出到控制台
  stdio: [process.stdin, process.stdout, process.stderr]
});

const fd = require("fs").openSync("./node.log", "w+");
child_process.spawn("node", ["-e", `( ${script}("整数fd,这一般个会写到某个文件") )`], {
  //stdio绑定到文件描述符fd,它代表文件node.log,因此会输出到该文件
  stdio: [process.stdin, fd, fd]
});

const writableStream = require("fs").createWriteStream("./node.log", {
  flags: "a",
  fd: fd
});
child_process.spawn("node", ["-e", `( ${script}("stream,这一般个会写到某个文件") )`], {
  //输出到流所代表的目的地,注意这个流必须要有文件描述符,否则会失败
  //这个例子中它会输出到文件node.log
  stdio: [process.stdin, writableStream, fd]
});
const script2 = (function(data) {
  console.log(data);
  process.stdout.end("hello");
}).toString();
const node = child_process.spawn("node", ["-e", `( ${script2}("pipe,只能通过父进程将它输出") )`], {
  //子进程的stdio绑定到父进程的
  stdio: ["pipe", "pipe", "pipe"]
});
node.stdout.on("data", function(data) {
  //注意此时子进程的输出全都通过管道传递到父进程,注意这种方式和"inherit"的区别
  console.log(String(data));
});

看文档时候我发现一个例子

// Open an extra fd=4, to interact with programs presenting a
// startd-style interface.
spawn('prg', [], { stdio: ['pipe', null, null, null, 'pipe'] });

stdio的值来看父子进程之间建立了额外的ipc通道,父进程可以很容易引用这些额外的ipc通道,但是我找了很久都没有发现子进程那边是如何使用这些额外的ipc通道,希望有大牛能告之。

detached和守护进程

生成子进程的时候如果传递了detached=true,则效果是使得子进程成为新的session和group的leader,效果和SETSID(2)是类似的。但也是仅此而已了,它和守护进程并没有明显的关联,文档中特别指出

Note that child processes may continue running after the parent exits regardless of whether they are detached or not

说明子进程是可以继续运行下去的,无论detached=true是否被设置,例如

"use strict";
const child_process = require("child_process");
child_process.spawn("ping", ["localhost"]);
setTimeout(function() {console.log("hello");}, 3000);
process.exit();

将上述代码保存到文件test.js,然后从命令行运行node test.js,则有以下输出

[chris@localhost node]$ node test.js 
[chris@localhost node]$ ps -ef | grep -E 'ping|node'
root       621     1  0 Mar03 ?        00:00:00 /usr/sbin/mcelog --ignorenodev --daemon --foreground
chris     6364     1  0 04:01 pts/0    00:00:00 ping localhost
chris     6366  5514  0 04:01 pts/0    00:00:00 grep --color=auto -E ping|node

父进程启动ping后就立即使用process.exit()强行退出,这使得ping成为孤儿进程并被init进程(进程id为1)收养,从而使得ping能够继续在后台运行,注意此时并没有设置detached=true,此时即使你退出终端ping命令依然在后台继续进程(有点像守护进程?)。
不过这种方法是有缺陷的,因为是强行退出的,父进程event loop中的内容尚未执行完退出了,我们需要一种优雅安全的退出方法。

首先删掉process.exit()并设置detached=true如果不设置的话子进程在父进程结束后也会跟着结束

"use strict";
const child_process = require("child_process");
child_process.spawn("ping", ["localhost"], {detached:true});
setTimeout(function() {console.log("hello");}, 3000);

此时setTimeout的回调可以得到执行,但是父进程会等待子进程退出,用ctrl+c结束父进程后子进程依然存活,并且被init进程收养。因为文档中说明:

By default, the parent will wait for the detached child to exit. To prevent the parent from waiting for a given subprocess, use the subprocess.unref() method

默认情况下父进程会等待已经分离的子进程,可以调用subprocess.unref()来取消等待。于是按照要求加上相关代码

"use strict";
const child_process = require("child_process");
const ping = child_process.spawn("ping", ["localhost"],{detached : true});
ping.unref();
setTimeout(function() {console.log("hello");}, 3000);

还是不行,父进程依然会等待子进程,再次查阅文档,原来还漏看了一句

unless there is an established IPC channel between the child and parent.

如果父子进程之间建立了的ipc,父进程还是会等待。根据我们上面的总结,推论将stdio设置为ignore、描述符、Stream对象应该可以让父进程不再等待。

"use strict";
const fd = require("fs").openSync("./node.log", "w+");
const writableStream = require("fs").createWriteStream("./node.log", {
	flags: "a",
	fd: fd
});
const child_process = require("child_process");
const ping = child_process.spawn("ping", ["localhost"], {
	detached: true,
	stdio: ["ignore", fd, writableStream]
});
ping.unref();
setTimeout(function() {
	console.log("hello");
}, 3000);

这次可以了,父进程在运行完自己的代码之后顺利退出,而子进程则继续在后台运行,同时被init进程收养

总结:要想让程序成为守护进程,必须要做到

  • 子进程必须要和父进程分离,即设置detached=true
  • 子进程要调用unref()
  • 子进程的io不能跟父进程有联系

execexecFile

先来看看exec的源码

function normalizeExecArgs(command, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }

  // Make a shallow copy so we don't clobber the user's options object.
  options = Object.assign({}, options);
  //如果指定了shell,则把它传递下去,否则将它设为true
  options.shell = typeof options.shell === 'string' ? options.shell : true;

  return {
    file: command,
    options: options,
    callback: callback
  };
}

exports.exec = function(command /*, options, callback*/) {
  var opts = normalizeExecArgs.apply(null, arguments);
  //其实就是简单调用execFIle
  return exports.execFile(opts.file,
                          opts.options,
                          opts.callback);
};

原来就是调用execFile,那么无需多言直接看execFile就可以了,值得一提的是文档中说exec会开启一个shell来执行命令,在代码中的体现是把options.shell设置为true,后续的函数根据这个来属性来决定是否开启一个shell执行命令。

再来看看exec的源码的关键部分

exports.execFile = function(file /*, args, options, callback*/) {
 //......	
 //直接调用spawn,但是传入了一些选项
  var child = spawn(file, args, {
    cwd: options.cwd,
    env: options.env,
    gid: options.gid,
    uid: options.uid,
    shell: options.shell,
    windowsHide: !!options.windowsHide,
    windowsVerbatimArguments: !!options.windowsVerbatimArguments
  });

  //......
  //调用完spawn之后会缓存子进程的stdout和stderr
  if (child.stdout) {
    if (encoding)
      child.stdout.setEncoding(encoding);//如果不是buffer类型,则是指编码

    child.stdout.on('data', function onChildStdout(chunk) {
    	//如果是buffer类型,则加上收到的字节数,否则,加上收到的字符串
      stdoutLen += encoding ? Buffer.byteLength(chunk, encoding) : chunk.length;

      if (stdoutLen > options.maxBuffer) {//判断是否超出内部的buffer,如果你的子进程的输出很大,请注意调整这个参数
        ex = new errors.RangeError('ERR_CHILD_PROCESS_STDIO_MAXBUFFER',
                                   'stdout');
        kill();
      } else if (encoding) {
        _stdout += chunk;//缓存字符串
      } else {
        _stdout.push(chunk);//缓存buffer
      }
    });
  }
  //......
  //监听子进程io流的关闭和子进程的错误,用户提供的callback会在这里被调用
  //上面被缓存的输出会当做参数传递过去
  child.addListener('close', exithandler);
  child.addListener('error', errorhandler);

  return child;
};

从上述代码可以清晰看到execFile就是调用了spawn并且将子进程的输出缓存起来然后通过回调一次过返回给用户spawn中是通过监听stdio上面的事件来获取子进程的输出(并且输出还不是一次返回),这有些繁琐。
execFile对其适当地封装使之变成了我们熟悉的回调方式,这应该就是execFile的优点
值得注意的是里面作为缓存的buffer是有默认大小的(默认为200 x 1024个字节),在项目中曾经试过因为子进程的输出太大导致抛出异常,因此execFile适合子进程的输出不是太大的情况,或者修改maxBuffer提供更大的缓存空间。

fork

fork的相关源码如下:

exports.fork = function(modulePath /*, args, options*/) {
 //省略	
 var execArgv;
  if (typeof options.stdio === 'string') {
    options.stdio = stdioStringToArray(options.stdio);
  } else if (!Array.isArray(options.stdio)) {
  	/*
  		这里的注释说明第4个元素的值是ipc,说明在父子进程之间除了stdio之外还有ipc通道可以进行通讯
  		详情可以见下面的stdioStringToArray函数
  	*/
    // Use a separate fd=3 for the IPC channel. Inherit stdin, stdout,
    // and stderr from the parent if silent isn't set.
    options.stdio = options.silent ? stdioStringToArray('pipe') :
      stdioStringToArray('inherit');
  } else if (options.stdio.indexOf('ipc') === -1) {
    throw new errors.Error('ERR_CHILD_PROCESS_IPC_REQUIRED','options.stdio');
  }
  //如果没有特地指定execPath则默认值为nodejs的启动路径的绝对值
  options.execPath = options.execPath || process.execPath;
  options.shell = false;
  return spawn(options.execPath, args, options);
}

function stdioStringToArray(option) {
  switch (option) {
    case 'ignore':
    case 'pipe':
    case 'inherit':
      return [option, option, option, 'ipc'];//第4个元素表示额外的ipc通道
    default:
      throw new errors.TypeError('ERR_INVALID_OPT_VALUE', 'stdio', option);
  }
}

fork也是相当简单,注意两个地方:

  • 父子进程之间建立了额外的ipc通道
  • 调用spawn的时候传递的第一个参数默认是nodejs路径的绝对值

这跟文档描述很契合,启动了一个独立nodejs子进程并且和它建立ipc通道

再看spawn

一开始我们就说明了,用户层面的spawn调用了nodejs内部的spawn来生成子进程,它们的源码如下:

var spawn = exports.spawn = function(/*file, args, options*/) {
  var opts = normalizeSpawnArguments.apply(null, arguments);
  var options = opts.options;
  var child = new ChildProcess();//一个内部的child_process构造函数,位于lib/internal/child_process.js
  debug('spawn', opts.args, options);
  child.spawn({//调用js内部的spawn函数,位于lib/internal/child_process.js
    //注意file和detached,待会看下c++是怎么使用它们
    file: opts.file,
    args: opts.args,
    cwd: options.cwd,
    windowsHide: !!options.windowsHide,
    windowsVerbatimArguments: !!options.windowsVerbatimArguments,
    detached: !!options.detached,
    envPairs: opts.envPairs,
    stdio: options.stdio,
    uid: options.uid,
    gid: options.gid
  });
  return child;
};

代码一目了然没什么好探讨的,可以说用户层面的child_process其实是内部child_process的一个封装,我们直接看内部的spawn

ChildProcess.prototype.spawn = function(options) {
  //省略...
  var err = this._handle.spawn(options);
  //省略...
  // Add .send() method and start listening for IPC data
  if (ipc !== undefined) setupChannel(this, ipc);
  return err;
};

内部的spawn看似很长但是核心代码就两句,分别用于生成子进程和建立父子进程的通讯通道。其中this._handle.spawn其实是调用了process_wrap.ccspawn,这属于C++层面的东西,我们下个章节会对它进行简要的分析。

子进程在底层如何生成

process_wrap.ccspawn

根据上面的分析,先来看process_wrap.ccspawn关键代码

  static void Spawn(const FunctionCallbackInfo<Value>& args) {
    //获取js传过来的第一个option参数
    Local<Object> js_options = args[0]->ToObject(env->context()).ToLocalChecked();

   //提取option里面的参数,例如file和detached
    // options.file
    Local<Value> file_v = js_options->Get(context, env->file_string()).ToLocalChecked();
    CHECK(file_v->IsString());
    node::Utf8Value file(env->isolate(), file_v);
    options.file = *file;

    // options.detached
    Local<Value> detached_v = js_options->Get(context, env->detached_string()).ToLocalChecked();

    if (detached_v->IsTrue()) {
      options.flags |= UV_PROCESS_DETACHED;
    }
    //调用uv_spawn生成子进程,并将父进程的event_loop传递过去
    int err = uv_spawn(env->event_loop(), &wrap->process_, &options);
    //省略
  }

它的主逻辑也很简单,仅仅看注释就很清楚,先是提取js传递过来的参数,然后调用process.cc中的 uv_spawn

process.cc中的 uv_spawn

终于来到了真正做事的地方:uv_spawn,它也是相当长,我们摘取核心部分来看看

int uv_spawn(uv_loop_t* loop,
             uv_process_t* process,
             const uv_process_options_t* options) {
  //省略一堆设置stdio的初始化工作代码
  err = uv__make_pipe(signal_pipe, 0);//建立父子进程之间的通讯通道,这个东西似乎是node内部使用的
  pid = fork();//用fork生成一个子进程

  if (pid == 0) {//如果是子进程,则执行uv__process_child_init然后退出
    uv__process_child_init(options, stdio_count, pipes, signal_pipe[1]);
    abort();
  }
  //省略...
  err = waitpid(pid, &status, 0);//等待子进程返回
  //如果是父进程则继续往下执行,以下是一堆相关的收尾释放资源之类的操作
}


static void uv__process_child_init(const uv_process_options_t* options,
                                   int stdio_count,
                                   int (*pipes)[2],
                                   int error_fd) {
  //哈哈,终于找到了设置detached=true的作用了
  if (options->flags & UV_PROCESS_DETACHED)
    setsid();
  //省略一大堆代码
  //最终是调用execvp来执行任务,注意最终运行的命令就是js中传递过来的file
  execvp(options->file, options->args);
}

从上面代码可以看到主要做了两件事:

  • uv_spawn调用了fork来生成子进程
  • 子进程调用execvp来执行新任务

(其实跟我们的预料是一样的,linux就提供了这两个函数,关于这个两个函数的具体用法大家请自行搜索,网上的介绍极其丰富,缺乏系统编程的知识真是不好读源码)

从注释可以看到js层面传递过去detached=truefile最终是如何被使用的。选取C++层面源码的时候我跳过了很多关于stdio的地方,这些细节我们都放在IPC再讨论(好吧,其实是好多没看明白,囧rz)

IPC通讯

本来想讨论一下父子进程之间的IPC包括stdio细节的,但是发现IPC足够写n篇文章了,以后再独立开吧。

一个文件分享的网站

$
0
0

前段时间被人问到有没有文件分享的网站,给他推荐了 WeTransfer,但是试用下来不太理想比较慢。想了想这个应该比较简单,于是我们自己写了一个文件分享的网站 https://bugu.link。项目是开源的分为前后端两个部分:后端项目前端项目。后端采用:Node.js + Koa2 + MySQL + Redis;前端采用:React + Yax + ReactRouter。

  • 支持分片上传
  • 支持断点续传
  • 支持大文件上传(暂时限制了单个文件最大 2G)
  • 上传速度快(简单测试了下 1G 文件上传只花了 4 分钟)
  • UI 简洁,使用简单
  • 默认文件保存 14 天
  • 分享链接默认带 Code

附个图: Screen Shot 2018-03-08 at 3.40.26 PM.png


Pandora.js 专业的 Node.js 应用监控管理器,阿里开源

$
0
0

TLDR

Github: http://github.com/midwayjs/pandora, 欢迎 PR、Issue 和 Star.

Pandora.js 阿里巴巴产出的一个Node.js 应用监控管理器,可以让您对自己的 Node.js 应用了若指掌,我们的目标就是让应用可管理、可度量、可追踪。

我们自 2014 年开始使用 Node.js 并参与运维工作,Pandora.js 是淘宝 Midway 团队这些年对企业环境 Node.js 运维监控的一个沉淀与总结。在发展的过程中,我们大量使用了社区的开源软件,这次 Pandora.js 的开源是对社区的回馈。希望 Pandora.js 能让 Node.js 更好的应用在专业的商业场景上,给予它更好的基础监控运维能力去服务大规模商业场景。

同时,这也是阿里巴巴开源的第一个使用了类型系统的 Node.js 软件。它使用的是 TypeScript,为 Pandora.js 带来了前所未有的逻辑健壮性。

欢迎您来体验,同时更欢迎您基于 Pandora.js 建构您的 Node.js 运维基础设施。

什么是 Pandora.js

就像前面提到的一样,Pandora.js 是一个 Node.js 应用监控管理器。它集成了多种类型的能力诸如:监控、链路追踪、调试、进程管理等等。一个一个讲太过凌乱,这里分为几个大范畴:

  1. 让业务更易追踪
    • 基于 Open-tracing 实现了业务链路追踪系统。
    • 实时追踪每个请求链路,让每次请求不再是黑盒。在运行时直观的看出接口或页面慢在哪里、错在哪里、超时在哪里。
    • 可以实时追踪多种基础中间件的用量及错误,诸如: MySQL、Mongose、Redis 等等。
  2. 让应用更易度量
    • 实现了软件行业上通用的 Metrics 系统。
    • 自带多种监控指标实现,从操作系统指标到 Node.js Runtime,从 HTTP QPS 到中间件用量。
    • 支持多种监控指标类型(Metrics 类型),您可以基于这些指标类型,轻松地建构您的业务监控指标。
  3. 让系统更加健壮
    • 故障演练扩展,可以模拟大部分基础中间件的不可以用状态。这是从淘宝多次大促中沉淀下来的断网演习工具。
    • 远程调试扩展,基于 Dashboard 的能力可以实时调试您的线上进程,线上业务问题不再抓瞎。
  4. 让应用更易管理
    • 进程管理管理能力,提供了基础的进程守护和 Cluster 能力。
    • 基础服务管理能力来满足中间件管理等场景,提供了依赖管理和标准的启停接口。
    • 提供了进程间通信的基础能力。

上面提到的全部能力和数据,全部通过 RESTFul 接口或文本日志透出,您可以轻松的将其集成进您的监控管理系统(为了更好的配套,在不远的将来我们也会将我们的私有监控管理平台开源)。

这样的概括还是有些枯燥与难以理解,接下来会通过 Pandora.js dashboard的界面介绍几个主要特性,Pandora.js dashboard 是一个与 Pandora.js 相配套的单机可视化仪表。然后在文章的最后是一个可以跑起来的例子~

业务链路追踪 - Trace

链路追踪是第一个要介绍的重量级特性,可以追踪每个业务请求的全过程,在运行时直观的看出接口或页面慢在哪里、错在哪里、超时在哪里。

听上去很厉害的样子,但实际使用起来很简单。您只需要简单的使用 Pandora.js 来启动您的应用,然后打开 Pandora.js dashboard 就可以看到您的业务链路的全部细节:

轻松识别慢链路和错误链路

undefined | center

调用了,哪里耗时多,一目了然

undefined | center

应用度量

默认的应用度量 - Built-in Metrics

Pandora.js 为您默认提供了近百项开箱即用的默认监控指标,让您轻松的开始监控您的应用。

默认提供了,Node.js Runtime 指标,操作系统指标,HTTP Incoming 指标等等。

Node.js 指标,包含大量运行时指标

undefined | center

操作系统指标,包含 Load、CPU、内存、磁盘、网络、TCP 等各种指标

undefined | center

自定义的应用度量 - Custom Metrics

应用运行在生产环境,我们有太多想要知道和观察的数据了。以往我们都是写很多日志文件,查看分析十分麻烦。Pandora.js 的 Metrics 提供了 Gauge(瞬时)、Counter(计数)、Meter(流速)、Histogram(直方图) 等多种度量类型,您可以轻易地完成大部分的应用或业务指标。

下面是一个例子:

根据 Email 去 Gravatar 获取用户的 Profile。然后我想统计有多少次成功了,多少次失败了。(这个例子在文章后面有附上)


const {MetricsClientUtil} = require('dorapan'); // dorapan 是 pandora 的一个客户端 sdk
const client = MetricsClientUtil.getMetricsClient(); // 获得 Metrics 客户端

// 创建两个 Counter (计数)类型的指标
const successCounter = client.getCounter('custom', 'custom.gravatar.success');
const failCounter = client.getCounter('custom', 'custom.gravatar.fail');

// ...此处省略若干代码

const userAvatars = {};
const promises = [];
for(const user of rows) {
  const profileUrl = gravatar.profile_url(user.email);
   promises.push(urllib.request(profileUrl, {
     followRedirect: true, dataType: 'json'
   }).then((res) => {
     if(typeof res.data === 'object') {
	   successCounter.inc(1);
       userAvatars[user.email] = res.data;
     } else {
	   failCounter.inc(1);
	 }
   }));
}
await Promise.all(promises);

可以看到 Metrics 中已经出现了 Custom 分组:

undefined | center

当然还有更多的 Metrics 类型可以实践,请具体参见文档

一个简单的例子

这个例子的演示依赖两个部件:

  1. Pandora.js 命令行工具:来启动和监控应用。
  2. Pandora.js dashboard:Pandora.js 的 单机版 GUI。

安装

您最好在 Linux 系统安装,同时 Pandora.js 依赖 Node.js >= 8.0.0 (也就是当前的 LTS 版本。我们在 macOS 上指标有部分实现也可以。如果您是 Windows 用户的话,我只能说欢迎 PR)。

  1. 安装 Pandora.js 命令行工具:
$ npm i pandora -g

安好后您会获得一个 pandora 命令。

  1. 安装 Pandora.js dashboard
$ npm i pandora-dashboard -g

然后您就可以启动 Pandora.js dashboard 了。

$ pandora start --name dashboard `pandora-dashboard-dir`

上面有个 command substitutionpandora-dashboard-dir,它是 Pandora.js 注入的一个全局命令。用来输出 Pandora.js dashboard 的目录,然后 Pandora.js 会把它当成一个普通的项目来启动。

安装完 Dashboard 之后打开 http://127.0.0.1:9081,您可以看到 Dashboard 这个应用,这就是 Pandora.js dashborad 自己。

启动一个简单的 MySQL CRUD 的例子

我找到一个 Node.js 的 简单 MySQL CRUD 的例子来进行接下来的演示。当然我为了演示还做了一些修改。示例项目在 Github,Clone 它您也可以跑跑试试看。

在这个例子里主入口为 server.js,在项目根目录运行下面的命令来初始化。

$ pandora init server.js 
? Which type do you like to generate ? (Use arrow keys)
  fork 
❯ cluster 

我选择了 Cluster,不过您可以选择任意一个,这两个选项的行为就像 PM2 一样。Fork 简单拉起 server.js ,而 Cluster 则用 Node.js 的 Cluster 模块启动 server.js (即 Master / Worker 模型)。

在初始化完后,将会生成一个 procfile.js文件。这个文件用来定义项目结构,就像 PM2 的 Process file 一样。其实 procfile 就是 Process File 的简写。

然后我们运行 pandora start来启动应用:

$ pandora start
Starting rest-crud at /xx/xxx/rest-crud
rest-crud started successfully! Run command [ pandora log rest-crud ] to get more information

先放问下 http://127.0.0.1:300/api/user查看下简单的 CRUD 例子,稍微做点操作。

然后让我们无视 Run command [ pandora log rest-crud ]这个提示,直接打开 http://127.0.0.1:9081/,然后您将看到:

undefined | center

点击 Standard Output按钮来查看控制台输出:

undefined | center

点击 Trace查看所有的链路:

undefined | center

点击 Metrics查看所有的 Metrics 指标:

undefined | center

这只是开始

Node.js 应用在运维方面依旧还是很薄弱,Pandora.js 所做的也只是一点点工作,帮忙开发者更加了解自己的应用,在问题来临时,不再迷惘,不再慌乱。

四年间,我们看到了 Node 的兴起,工具链的完善,也看到了应用场景的萎缩,产品的各种迭代和意外,唏嘘之余,依旧得做好分内之事,让 Node 在部门,在集团,乃至在业界都能有一些成长和突破,同时让自己看清和选择一条路,坚定的走下去。

最后,再一次欢迎您来体验,尝试基于 Pandora.js 来建构您的 Node.js 运维基础设施,让天下没有难管理的应用。

【转载】互联网垃圾信息的终极终结者

$
0
0

我们如何玩好互联网信息传播这个每天都重复的“囚徒游戏”呢?

由于互联网信息传播中信息提供者特征可识别性与其传播的信息历史可查询性,我们根据我们的经验为与我们打交道的信息提供者设置相应的权值,这里称为信誉权值。信誉权值越高说明信息提供者提供的信息就对于我们来说越有价值,我们就可以奖励回报为我们提供高价值信息的信息提供者与反馈提高其信誉权值。信誉权值越低说明信息提供者提供的信息就对于我们来说越没有价值,我们就可以惩罚隔离这些提供垃圾信息的信息提供者与反馈降低其信誉权值。

通过为信息提供者设置的个人可随时修改的信誉权值,我们可以非常有效率的与愿意提供高价值信息的高信誉权值的信息提供者达成信息传播的“合作”,拒绝并惩罚那些提供垃圾信息的低信誉权值的信息提供者的信息传播的“合作”。为了互联网信息传播这个天天重复的“囚徒游戏”能更好长久的玩下去,我们需要如《合作的进化》中的所有善良策略者一样,不能耍小聪明地对善良的高信誉权值的信息提供者的高价值信息传播的“合作”予以善良的回报。

详细地址: https://github.com/turenmianshi/turenmianshi/wiki/book3_chshttps://turenmianshi.github.io/turenmianshi/book3_chs.html

如果你也深受垃圾信息的荼毒,想让更多的相关的人看到,请转发

发现一个非常方便的录屏工具可以转换gif

[北上杭] 蚂蚁金服招前端,各岗位内推,实习社招不限

$
0
0

职位要求:

  1. 精通 HTML5、CSS3 和 JavaScript 语言,掌握 HTTP 及相关网络协议,熟悉跨终端、跨浏览器的开发模式和平台特性,了解业界技术发展状况;
  2. 熟悉模块化、前端编译和构建工具,熟练运用主流的移动端 JS 库和开发框架,有基于 vue 或者 react 框架开发的产品,了解 jquery/vue/angular/react 等常用前端类库/框架的设计原理;
  3. 两年以上前端相关工作经验,对一到多个专项技术领域有较深入研究;
  4. 掌握一种非前端开发语言并实际参与过项目;
  5. 有优质的技术组件产出或开源产品者优先;
  6. 有中大型网站前端架构、H5 应用的体验与性能优化、Hybrid 模式应用开发、Native 应用开发等经验可作为加分项;
  7. 充分的产品负责人意识和项目把控能力。

前端、算法、研发 Java、研发 C/C++ 岗位不限。

其他岗位公司也有招聘,具体职位详情请戳: https://campus.alibaba.com/positionList.htm感兴趣的邮件 bluetomlee@gmail.com

仅有1.4kb ! 兼容全部浏览器的组件化渲染插件 Eng-NOS

$
0
0

Eng-NOS

  • 当前仅有 1.4Kb的体积 ,却兼容所有浏览器 , 是传统网页模式 实现组件化数据渲染的利器 ,只需学习4个指令就能完成所有操作
  • 是对于传统页面 有兼容需求 ,又渴望组件化渲染 , 还想要极低学习曲线项目的 实现利器 …
  • Eng-NOS 寓意为 Eng的 NOS 氮气直线加速系统 , 因此舍弃了对完整Eng的其它功能支持 , 只做单纯的页面渲染
  • 只渲染Eng的 e-base、e-attr、e-html、e-for 4个指令 , (e-attr 可操作所有行内属性) , 实时上绝大部分常规页面也仅做渲染 , 使用其它js工具操作, Eng-NOS恰恰最符合这种应用场景
  • 拥有世界顶尖的性能 远超所有主流已知同类JS工具. 是 Eng和其同类工具中佼佼者的完整渲染性能耗时的1/3左右, 纯html文本用时占比 1/10左右
  • 可以和 Eng共享组件 , 但仅能执行 被 Eng-Drive编译后的组件 , 被编译后的组件体积相较于原始会有所增加, 但相对于组件本身的大小,几乎可以忽略

版本说明

  • 0.9 (当前) 版本, 仅支持Eng的 e-base、e-attr、e-html、e-for
  • 1.0 版本 待考虑

声明

  • Eng致力于精简所有同类工具中的技术玄学 , 用最 简明、简易和最少的API实现同类工具的核心部分 ,摒除一切冗余的学习内容,无谓的蹉跎,并达成同样的目的.

使用示例 :

   var  engCompileData= .... ;
   var  data={
              .......
            };
   var html= Eng_Nos( data , engCompileData );
            
      	document.body.innerHTML=html;
      	// 没了就这样 ( 仓库里 有个 demo 源码在页面内)

License

License

PingPong 金融杭州招聘 - 招聘高级前端工程师- 杭州滨江西兴地铁

$
0
0

PingPong 金融是家极其低调又极具实力的独角兽公司,作为中国第一家获得欧洲支付牌照的民营公司,目前在跨境收款这个风口处于领先地位,B 轮融资 4 个亿,表示不差钱。先晒JD:

前端工程师职位描述(薪资 15-30K):

岗位职责 1.与设计师、产品经理、后端工程师紧密工作,实现产品 UI 和交互方面的开发需求,理解产品经理和设计师; 2.能高效完成产品的迭代和新需求的开发; 3.根据业务抽离出组件,进行组件化开发; 4.优化团队开发方式及流程、提升团队开发效率、推动团队技术发展;

岗位要求

1.3 年以上前端开发经验(工作优秀者至少 2 年以上) 2.精通主要的前端语言,熟悉页面架构和布局,能够准确细致规划 CSS 结构,熟悉网站 Div+CSS 标准化布局,制作符合 w3c 规范的页面,能够解决各种浏览器兼容性问题; 3.熟练使用HTML5/CSS3/nodejs; 4.熟练使用 Vue 以及其工作原理,熟悉 Vue 组件化; 5.熟悉 Webpack 等工具; 6.具备良好的表达能力,善于团队合作、具备非常良好的责任心以及 Owner 意识,具备独立解决问题能力;

有意向的请发简历到邮箱: jiwg#pingpongx.com,

其他岗位也招,请参考帖子: V2EX招聘

最后感谢各位的阅读.

最近在学习NODE 权威指南这本书遇到一点问题求大神帮忙

$
0
0

学习到中间件connect时,提到他自带了两个模块 一个是logger 这个我上网搜索到已经分出改成morgan模块 那么另外一个是favicon,现在报错如下 TypeError: connect.favicon is not a function at Object.<anonymous> (/Users/johnny/Downloads/code/6/http.js:5:26) at Module._compile (module.js:660:30) at Object.Module._extensions…js (module.js:671:10) at Module.load (module.js:573:32) at tryModuleLoad (module.js:513:12) at Function.Module._load (module.js:505:3) at Function.Module.runMain (module.js:701:10) at startup (bootstrap_node.js:193:16) at bootstrap_node.js:617:3 johnnydeMacBook-Air:6 johnny$ 有木有大神告诉我 是什么原因…懵逼中额么么么


nunjucks模板的使用,网页html文件放入到views中的新建文件夹后,无法继承?

$
0
0

图片.png index.js已经更改了 res.render(‘main/index’, { title: ‘Express’ });
图片.png继承语法没有问题 图片.png将html文件从main文件夹拿出来后就可以继承了~ 这继承出现问题是什么情况呢 error报错: 图片.png

用koa-session这个包却获取不到ctx.session的值

$
0
0

用koa2写接口,我在登陆成功的接口里写了类似如下代码

exports.signIn = async (ctx, next) => { //如果登陆成功 ctx.session.user=’…’ }

但是接下来我在另一个接口中却获取不到ctx.session这个值,中间件的使用方式以及options都与官网一致,看样子也是支持koa2的

const session = require(‘koa-session’); const Koa = require(‘koa’); const app = new Koa();

app.keys = [‘some secret hurr’];

const CONFIG = { key: ‘koa:sess’, /** (string) cookie key (default is koa:sess) / /* (number || ‘session’) maxAge in ms (default is 1 days) / /* ‘session’ will result in a cookie that expires when session/browser is closed / /* Warning: If a session cookie is stolen, this cookie will never expire / maxAge: 86400000, overwrite: true, /* (boolean) can overwrite or not (default true) / httpOnly: true, /* (boolean) httpOnly or not (default true) / signed: true, /* (boolean) signed or not (default true) / rolling: false, /* (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. (default is false) / renew: false, /* (boolean) renew session when session is nearly expired, so we can always keep user logged in. (default is false)*/ };

app.use(session(CONFIG, app)); // or if you prefer all default config, just use => app.use(session(app));

环境如下:

“node”: "8.9.3" “koa”: “^2.2.0”, “koa-session”: “^5.0.0”

请问是哪里出了问题么

关于windows的问题 NetStat -ano |findstr 查看端口占用

$
0
0

为什么cmd下执行NetStat -ano |findstr xxxx 查看xxxx端口的时候会出现一堆进程 正常貌似只有一俩个 11111.png但是 去服务器上查看了下服务器的端口的时候却出来一堆 (网上找的图) 1438679325_920918.png类似这样的 是因为这样的,一直使用pm2 管理node进程,但是经常restart的是报错出来 重复监听端口, 有一次程序运行很卡很慢,有个方法是加了redis分布式锁的,正常情况下这个锁是毫秒级别消失的,但是那天却一直是自己锁过期时间才释放,感觉node运行不正常,然后就重启app 用pm2 restart一下,然后就报重复监听了,然后就用命令行强制杀死这个端口的进程。之后这个redis分布式锁就恢复正常了,说明程序也正常了? ps:初步估计是可能有流程没有callback…不知道是不是这个原因? 各位大佬有什么看法?

如何获取V8引擎内存,进程,GC等信息呢?

$
0
0

之前用过heap-dump 查找过内存泄漏,想问一下对于node应用来说像heap-dump,memcache 等npm包 是如何获取到V8 内存信息的呢? 还有如果想监控性能比如进程层面的堆内存情况,GC 时间,cpu使用等信息那要怎么获取呢? 是底层V8 暴露一些接口了么?

[上海] nodejs,不加班,可接受JS,Java,Ruby转,薪资15k-30k

$
0
0

美资外企,不加班,员工友好,上下级和睦,薪资15k-30k,福利多多

工作地点 上海市浦东新区张江高科

招聘要求 本身就是nodejs开发,要求有2年以上相关经验;如果是转的,要求熟悉nodejs开发,并对nodejs很有兴趣 英语可以简单沟通,或nodejs基础扎实,可以不要求英语 快速学习能力

薪水待遇 看个人水平,如果你经验丰富,欢迎挑战高薪 如果你只有1,2年经验也没关系,这里有经验丰富的人带你,只要你可以快速学习 待遇范围(15k-30k) * 13薪 + 年终奖(1-2个月)+补助,上班不打卡,美资企业福利不用担心

职业发展 如果你是希望在金融领域发展的话,这里很适合,也会有区块链的项目 客户方都很友好,不存在加班压力 如果你将来走职业经理人路线,这里也很适合你

联系方式 Email: yolandali@zwhrconsulting.com(邮箱简历给我) Wechat: 570962030

Viewing all 14821 articles
Browse latest View live