小鱼二的博客, 喜欢请点赞 :star
xiaoyu2er / blog Goto Github PK
View Code? Open in Web Editor NEW小鱼二的博客, 喜欢的话请点star :D
小鱼二的博客, 喜欢的话请点star :D
在上一篇易企秀H5项目重构总结 中, 笔者详细记录了重构项目的缘由, 技术选型及实践, 灰度及 bug 修复, 编辑器功能边界划分及模块化实践, code review 实践 及 总结.
距离上次总结已经过去了一年的时间, 在这段时期, 笔者又基于当初的项目架构, 横向扩展了 wap 编辑器和 混合模式编辑器, 其中 wap 编辑器在线上正常运营, 而混合模式则碰到了很多技术瓶颈, 虽达到测试上线标准, 但是最终夭折腹中, 没有正式上线; 除此之外, 我与明纯 共同重构了另外一项复杂业务: 组合, 图层和功能模板. 最近, 有关组合, 图层的设计和一些复杂的算法明纯会在他的博客分享, 敬请期待 :D
回过头看上一篇总结, 有些**和方法是非常具有前瞻性的, 有些决定则看起来缺乏魄力, 显得略显保守. 接下来, 笔者将会就第一次重构进行复盘, 对目前项目的构建过程, 核心组件库, 编辑器架构等方面做最后一次易企秀 H5 项目的架构梳理.
所有编辑器都是相对独立的单页 app, 使用 webpack 进行构建
H5 浏览项目构建流程相对复杂, 采取 webpack 处理程序逻辑打包 + gulp 处理流程 的方式 进行构建
顶级目录
├── README.md 项目说明
├── build 构建目录
├── docs 文档目录
├── env 环境目录
├── eqxiu.json 自动化部署配置
├── gulpfile.view.js H5预览 gulp 配置
├── node_modules
├── package.json
├── src 源文件目录
├── tsconfig.json typescript 配置
├── types
├── util.js 构建工具的 util
├── webpack.config.js webpack
├── webpack.helper.js 公共 webpack 配置
├── webpack.view.js H5 预览 webpack 配置
源码目录
src
├── appWpEditor app 混合模式
├── common js common
├── core 核心组件库
├── editor 编辑器
├── metaWpEditor 贺卡编辑器
├── ngcommon ng common
├── view H5 view
└── wpeditor wap 编辑器
核心组件库维护了所有易企秀H5的所有核心组件及其内在逻辑
view预览步骤
参考 易企秀H5项目重构总结 编辑器功能边界划分及模块化实践, 核心**并未发生变化, 摘录如下
编辑器理念
经过项目拆分, 笔者划定了项目的整个边界
包括 模板, 素材, 页面管理, 编辑, 场景设置, 设计师功能等.
其中涉及到编辑组件的功能边界如下
从2016年6月开始, 整个项目规模已经达到20w 行代码的规模, 作为易企秀的核心业务, 日 pv 稳定在1亿左右, 是个非常值得骄傲的成绩.
自2017年1月开始, 整个项目就按照当初设想的那样, core+多编辑器实例, 没有太大的变化, 至此笔者有些欣慰, 也有些忧伤.
欣慰的是一年多前的重构架构可以支撑后来一年的迭代, 说明笔者架构的整体思路没有太大问题. 忧伤的也正在此, 笔者知道, 这个架构还远远不够, 还有很多地方可以优化调整. 然而并没有很多大的改动.
当你看到这篇文章时, 笔者已经离开易企秀了, 希望后继者可以提出更加健壮的方案, 助力易企秀再上新台阶.
在天地之间找一个自己的位置
参与微贷事业部 花呗、借呗、网商银行 等金融业务产品线研发,参与移动端 H5、React、Node.js 应用框架体系建设以及前端工程体系建设。
熟练掌握移动端 H5 开发、熟悉主流移动浏览器的技术特点;
熟练运用 JavaScript 语言与 HTML5、CSS3 等技术;
熟悉模块化、前端编译和构建工具,熟练运用主流的移动端 JS 库和开发框架,并深入理解其设计原理,例如:Zepto、React 等;
能提供完善的 WebApp 技术方案,了解 native 移动应用开发,有类 react native 开发经验者优先;
对技术有强烈的进取心,具有良好的沟通能力和团队合作精神、优秀的分析问题和解决问题的能力。
熟悉 Node.js Web 应用开发,有大型 Node.js 项目的开发经验;
熟悉 Node.js 异步编程,对 koa/co/async 等模块原理机制了解透彻;
熟悉 Node.js 以及 V8 的性能和稳定性优化,能对系统整体性能进行评估,解决内存瓶颈;
熟悉 Web 安全相关知识,并能使用相关技术防范安全漏洞;
关注业内动态,有开源社区贡献者优先;
个性乐观开朗,逻辑性强, 具有良好的沟通能力和团队合作精神
熟练掌握 Javascript,有移动端 H5 研发基础
精通 Canvas/WebGL/CSS3,并能够使用原生 API 绘制图形动画
良好的前端架构能力和项目管理能力,良好的编码习惯,掌握图形图像学知识基础
有 H5 互动游戏开发经验、3D 游戏开发经验优先
对技术有强烈的进取心,具有良好的沟通能力和团队合作精神、优秀的分析问题和解决问题的能力
可以发简历到 [email protected]
邮件标题:简历-姓名-前端-github
附上你的简历
可以在邮件中写明你希望 base 的地点(北京、杭州)和你目前所在的城市。
Gulp是一个非常棒的自动化构建工具, 笔者在自己的实践和团队分享过程中, 总结整理一系列的有关 gulp 的资料. automating-your-workflow-with-gulp 就是这个么一个 repo.
这个 repo 中包括了:
如果你对 gulp 感兴趣, 可以和我一起整理相关资料
automating-your-workflow-with-gulp <-- 戳他 戳他 😃
这两天读了公司的一本书 <带人的技术>
读罢做一些抄录和记录一些自己的看法, 欢迎大家就管理, 团队建设, Tech Lead 等话题进行讨论 😄
笔者坚信任何技能都是可习得的, 虽然笔者是技术出身, 但是喜欢分享,
希望以后可以管理一支自己的团队.
所以未来也会看一些管理类的书籍, 做一些理论积累.
也希望未来有了管理经历之后再总结自己的一套管理方法论.
欢迎交流!
小 N 同学通讯录太多,希望可以导出到 Excel 中,网上大部分做法都需要安装软件或者还有自己整理数据,太麻烦。
我们来试一试可不可以通过前端的思路来解决这个问题。
既然是前端工程师,那么最简单的方式就是登录微信 web 版, 直接去看微信活动通讯录的接口。
我们看一下
可以看到,由于微信后端返回数据中中文出现了乱码,暂且没有想好如何处理这些乱码。
但是界面中的中文确实正常的,我们看一下有没有其他别的方法呢?
通过看网页结构,我们发现这是一个用 angularjs 1.x 编写的网页应用(出现了ng-style
ng-repeat
等关键词)
我们知道 angularjs 中的双向绑定,一般变量都会挂在 scope 上
既然所有的联系人都出现在了 $('.scroll-wrapper .J_ContactScrollBody')
中,那我们看看这个列表关联的 scope 中是否含有这些信息
var scope = angular.element($('.scroll-wrapper .J_ContactScrollBody')).scope();
var allContacts = scope.allContacts;
果不其然,在 $('.scroll-wrapper .J_ContactScrollBody')
关联的 scope 上挂载有 allContracts
(其实在观察的过程中,发现微信的工程师把所有的联系人信息放到了 window._contracts
上了,这也是一种方法。)
拿到数据之后接下来就是导出到 Excel 了,这里选用了 js-xlsx 库,其中的细节不再赘述
直接看一下源码
// 点击通讯录 tab
$('.web_wechat_tab_friends').click();
// 等待几秒钟...
// 获取通讯录列表
// 方法一
var scope = angular.element($('.scroll-wrapper .J_ContactScrollBody')).scope();
var allContacts = scope.allContacts;
// 方法二
// var allContacts = Object.keys(window._contacts).map(k => window._contacts[k]);
// 过滤真实的用户
var contacts = allContacts.filter(c => c.UserName);
// 下载 excel 脚本
loadScript('https://oss.sheetjs.com/js-xlsx/xlsx.full.min.js')
.then(() => {
console.log('download js-xlsx successful ');
var config = {bookType: 'xlsx', bookSST: false, type: 'binary'};//这里的数据是用来定义导出的格式类型
var wb = {SheetNames: ['Sheet1'], Sheets: {}, Props: {}};
// 通过json_to_sheet 转成单页(Sheet)数据
wb.Sheets['Sheet1'] = XLSX.utils.json_to_sheet(formatContacts(contacts));
var fileName = '微信通讯录' + '.' + (config.bookType == "biff2" ? "xls" : config.bookType);
saveAs(new Blob([s2ab(XLSX.write(wb, config))], {type: 'application/octet-stream'}), fileName);
});
// ---- helper functions -----
/**
* 将 contacts 转化成你需要的格式
* 这里可以任意发挥
* @param contacts
* @returns {*}
*/
function formatContacts(contacts) {
return contacts.map(({NickName, Sex, RemarkName}) => {
return {
'昵称': NickName,
'备注': RemarkName
}
})
}
/**
* 加载 script
* @param url
* @returns {Promise}
*/
function loadScript(url) {
return new Promise((resolve) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.onload = resolve;
script.src = url;
head.appendChild(script);
})
}
/**
* 下载文件
* @param obj
* @param fileName
*/
function saveAs(obj, fileName) {
var a = document.createElement('a');
a.download = fileName || '下载';
a.href = URL.createObjectURL(obj);
a.click(); // 模拟点击实现下载
setTimeout(function () {
URL.revokeObjectURL(obj); // 释放 objectURL
}, 100);
}
/**
* 字符串转字符流
* @param s
* @returns {ArrayBuffer}
*/
function s2ab(s) {
var buf = new ArrayBuffer(s.length);
var view = new Uint8Array(buf);
for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
return buf;
}
自由的接触信息应该是每个人不可剥夺的基本权利.
每周更新!
敬请关注! google-mirror-check npm包已发布
如果您是以下镜像的作者, 且不希望出现在以下列表中,可以联系我移除链接。
实现一个 Lazyman:
LazyMan(“Hank”)输出:
Hi! This is Hank!LazyMan(“Hank”).sleep(10).eat(“dinner”)输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~LazyMan(“Hank”).eat(“dinner”).eat(“supper”)输出
Hi This is Hank!
Eat dinner~
Eat supper~
LazyMan(“Hank”).sleepFirst(5).eat(“supper”)输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper
function LazyMan(name) {
return new _LazyMan(name);
}
class _LazyMan {
constructor(name) {
this.tasks = [];
this.canExecute = true;
var task = function lazyMan(cb) {
console.log(`Hi This is ${name}`);
cb();
};
this.addTask(task);
}
eat(food) {
var task = function eat(cb) {
console.log(`Eat ${food}~`);
cb();
}
this.addTask(task);
return this;
}
sleep(sec) {
var task = function sleep(cb) {
setTimeout(() => {
console.log(`Wake up after ${sec}`);
cb();
}, sec * 1000);
};
this.addTask(task);
return this;
}
sleepFirst(sec) {
var task = function sleepFirst(cb) {
setTimeout(() => {
console.log(`Wake up after ${sec}`);
cb();
}, sec * 1000);
};
this.addTask(task, true);
return this;
}
addTask(task, first) {
if (first) {
this.tasks.unshift(task);
} else {
this.tasks.push(task);
}
this.executeTasks();
}
executeTasks() {
setTimeout(() => {
if (!this.canExecute) return;
this.canExecute = false;
var task = this.tasks.shift();
if (task) {
// console.log(task.name);
task(() => {
this.canExecute = true;
this.executeTasks();
});
}
}, 0);
}
}
// LazyMan('Hank');
// LazyMan('Hank').eat('dinner').eat('supper')
// LazyMan('Hank').sleep(2).eat('dinner');
// LazyMan('Hank').eat('dinner').eat('supper');
LazyMan('Hank').sleepFirst(2).eat('supper');
原题来自: javascript-puzzlers
读者可以先去做一下感受感受. 当初笔者的成绩是 21/44...
当初笔者做这套题的时候不仅怀疑智商, 连人生都开始怀疑了....
不过, 对于基础知识的理解是深入编程的前提. 让我们一起来看看这些变态题到底变态不变态吧!
["1", "2", "3"].map(parseInt)
知识点:
首先, map接受两个参数, 一个回调函数 callback, 一个回调函数的this值
其中回调函数接受三个参数 currentValue, index, arrary;
而题目中, map只传入了回调函数--parseInt.
其次, parseInt 只接受两个两个参数 string, radix(基数).
在没有指定基数,或者基数为 0 的情况下,JavaScript 作如下处理:
- 如果字符串 string 以"0x"或者"0X"开头, 则基数是16 (16进制).
- 如果字符串 string 以"0"开头, 基数是8(八进制)或者10(十进制),那么具体是哪个基数由实现环境决- 定。ECMAScript 5 规定使用10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出radix参数的值。
- 如果字符串 string 以其它任何值开头,则基数是10 (十进制)。
所以本题即问
parseInt('1', 0);
parseInt('2', 1);
parseInt('3', 2);
首先后两者参数不合法.
所以答案是 [1, NaN, NaN]
[typeof null, null instanceof Object]
两个知识点:
typeof 返回一个表示类型的字符串.
instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上.
这个题可以直接看链接... 因为 typeof null === 'object'
自语言之初就是这样....
typeof 的结果请看下表:
type result
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Symbol "symbol"
Host object Implementation-dependent
Function "function"
Object "object"
所以答案 [object, false]
[ [3,2,1].reduce(Math.pow), [].reduce(Math.pow) ]
知识点:
arr.reduce(callback[, initialValue])
reduce接受两个参数, 一个回调, 一个初始值.
回调函数接受四个参数 previousValue, currentValue, currentIndex, array
需要注意的是 If the array is empty and no initialValue was provided, TypeError would be thrown.
所以第二个表达式会报异常. 第一个表达式等价于 Math.pow(3, 2) => 9; Math.pow(9, 1) =>9
答案 an error
var val = 'smtg';
console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');
两个知识点:
简而言之 +
的优先级 大于 ?
所以原题等价于 'Value is true' ? 'Somthing' : 'Nonthing'
而不是 'Value is' + (true ? 'Something' : 'Nonthing')
答案 'Something'
var name = 'World!';
(function () {
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
这个相对简单, 一个知识点:
在 JavaScript中, functions 和 variables 会被提升。变量提升是JavaScript将声明移至作用域 scope (全局域或者当前函数作用域) 顶部的行为。
这个题目相当于
var name = 'World!';
(function () {
var name;
if (typeof name === 'undefined') {
name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
所以答案是 'Goodbye Jack'
var END = Math.pow(2, 53);
var START = END - 100;
var count = 0;
for (var i = START; i <= END; i++) {
count++;
}
console.log(count);
一个知识点:
在 JS 里, Math.pow(2, 53) == 9007199254740992 是可以表示的最大值. 最大值加一还是最大值. 所以循环不会停.
补充: @jelly7723
js中可以表示的最大整数不是2的53次方,而是1.7976931348623157e+308。
2的53次方不是js能表示的最大整数而应该是能正确计算且不失精度的最大整数,可以参见js权威指南。
9007199254740992 +1还是 9007199254740992 ,这就是因为精度问题,如果 9007199254740992 +11或者 9007199254740992 +111的话,值是会发生改变的,只是这时候计算的结果不是正确的值,就是因为精度丢失的问题。
var ary = [0,1,2];
ary[10] = 10;
ary.filter(function(x) { return x === undefined;});
答案是 []
看一篇文章理解稀疏数组
我们来看一下 Array.prototype.filter 的 polyfill:
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun/*, thisArg*/) {
'use strict';
if (this === void 0 || this === null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
if (typeof fun !== 'function') {
throw new TypeError();
}
var res = [];
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
for (var i = 0; i < len; i++) {
if (i in t) { // 注意这里!!!
var val = t[i];
if (fun.call(thisArg, val, i, t)) {
res.push(val);
}
}
}
return res;
};
}
我们看到在迭代这个数组的时候, 首先检查了这个索引值是不是数组的一个属性, 那么我们测试一下.
0 in ary; => true
3 in ary; => false
10 in ary; => true
也就是说 从 3 - 9 都是没有初始化的'坑'!, 这些索引并不存在与数组中. 在 array 的函数调用的时候是会跳过这些'坑'的.
var two = 0.2
var one = 0.1
var eight = 0.8
var six = 0.6
[two - one == one, eight - six == two]
IEEE 754标准中的浮点数并不能精确地表达小数
那什么时候精准, 什么时候不经准呢? 笔者也不知道...
答案 [true, false]
function showCase(value) {
switch(value) {
case 'A':
console.log('Case A');
break;
case 'B':
console.log('Case B');
break;
case undefined:
console.log('undefined');
break;
default:
console.log('Do not know!');
}
}
showCase(new String('A'));
两个知识点:
switch 是严格比较, String 实例和 字符串不一样.
var s_prim = 'foo';
var s_obj = new String(s_prim);
console.log(typeof s_prim); // "string"
console.log(typeof s_obj); // "object"
console.log(s_prim === s_obj); // false
答案是 'Do not know!'
function showCase2(value) {
switch(value) {
case 'A':
console.log('Case A');
break;
case 'B':
console.log('Case B');
break;
case undefined:
console.log('undefined');
break;
default:
console.log('Do not know!');
}
}
showCase2(String('A'));
解释:
String(x) does not create an object but does return a string, i.e. typeof String(1) === "string"
还是刚才的知识点, 只不过 String 不仅是个构造函数 直接调用返回一个字符串哦.
答案 'Case A'
function isOdd(num) {
return num % 2 == 1;
}
function isEven(num) {
return num % 2 == 0;
}
function isSane(num) {
return isEven(num) || isOdd(num);
}
var values = [7, 4, '13', -9, Infinity];
values.map(isSane);
一个知识点
此题等价于
7 % 2 => 1
4 % 2 => 0
'13' % 2 => 1
-9 % % 2 => -1
Infinity % 2 => NaN
需要注意的是 余数的正负号随第一个操作数.
答案 [true, true, true, false, false]
parseInt(3, 8)
parseInt(3, 2)
parseInt(3, 0)
第一个题讲过了, 答案 3, NaN, 3
Array.isArray( Array.prototype )
一个知识点:
一个鲜为人知的实事: Array.prototype => []
;
答案: true
var a = [0];
if ([0]) {
console.log(a == true);
} else {
console.log("wut");
}
解析:
Boolean([0]) === true
答案: false
[]==[]
[] 是Object, 两个 Object 不相等
答案是 false
'5' + 3
'5' - 3
两个知识点:
+
用来表示两个数的和或者字符串拼接, -
表示两数之差.
请看例子, 体会区别:
> '5' + 3
'53'
> 5 + '3'
'53'
> 5 - '3'
2
> '5' - 3
2
> '5' - '3'
2
也就是说 -
会尽可能的将两个操作数变成数字, 而 +
如果两边不都是数字, 那么就是字符串拼接.
答案是 '53', 2
1 + - + + + - + 1
这里应该是(倒着看)
1 + (a) => 2
a = - (b) => 1
b = + (c) => -1
c = + (d) => -1
d = + (e) => -1
e = + (f) => -1
f = - (g) => -1
g = + 1 => 1
所以答案 2
var ary = Array(3);
ary[0]=2
ary.map(function(elem) { return '1'; });
稀疏数组. 同第7题.
题目中的数组其实是一个长度为3, 但是没有内容的数组, array 上的操作会跳过这些未初始化的'坑'.
所以答案是 ["1", undefined × 2]
这里贴上 Array.prototype.map 的 polyfill.
Array.prototype.map = function(callback, thisArg) {
var T, A, k;
if (this == null) {
throw new TypeError(' this is null or not defined');
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
if (arguments.length > 1) {
T = thisArg;
}
A = new Array(len);
k = 0;
while (k < len) {
var kValue, mappedValue;
if (k in O) {
kValue = O[k];
mappedValue = callback.call(T, kValue, k, O);
A[k] = mappedValue;
}
k++;
}
return A;
};
function sidEffecting(ary) {
ary[0] = ary[2];
}
function bar(a,b,c) {
c = 10
sidEffecting(arguments);
return a + b + c;
}
bar(1,1,1)
这是一个大坑, 尤其是涉及到 ES6语法的时候
知识点:
首先 The arguments object is an Array-like object corresponding to the arguments passed to a function.
也就是说 arguments
是一个 object
, c 就是 arguments[2], 所以对于 c 的修改就是对 arguments[2] 的修改.
所以答案是 21
.
然而!!!!!!
当函数参数涉及到 any rest parameters, any default parameters or any destructured parameters
的时候, 这个 arguments 就不在是一个 mapped arguments object
了.....
请看:
function sidEffecting(ary) {
ary[0] = ary[2];
}
function bar(a,b,c=3) {
c = 10
sidEffecting(arguments);
return a + b + c;
}
bar(1,1,1)
答案是 12
!!!!
请读者细细体会!!
var a = 111111111111111110000,
b = 1111;
a + b;
答案还是 111111111111111110000
. 解释是 Lack of precision for numbers in JavaScript affects both small and big numbers.
但是笔者不是很明白................ 请读者赐教!
var x = [].reverse;
x();
这个题有意思!
知识点:
The reverse method transposes the elements of the calling array object in place, mutating the array, and returning a reference to the array.
也就是说 最后会返回这个调用者(this), 可是 x 执行的时候是上下文是全局. 那么最后返回的是 window
.
补充:
@stellar91 这个笔者实践了一下 发现 firefox 是 window, chrome 报错
VM190:2 Uncaught TypeError: Array.prototype.reverse called on null or undefined(…)
可能是实现不同, 在 chrome 中应该是对调用者做了检查.
答案是 window
Number.MIN_VALUE > 0
true
@10081677wc
MIN_VALUE 属性是 JavaScript 中可表示的最小的数(接近 0 ,但不是负数),它的近似值为 5 x 10-324。
今天先到这里, 下次我们来看后22个题!
100*100的 canvas 占多少内存?
在 三年前端,面试思考 中提到了一个题目,非常有新意,这里分享一下当时面试的思考过程。
其实真正的答案是多少我并不清楚,面试过程中面试官也不期待一个准确的答案,而是看你的思考过程。
如果了解过 Canvas 且做过滤镜相关的工作,可能调用过 imageData = ctx.getImageData(sx, sy, sw, sh);
这个 API。我记得这个 API 返回的是一个 ImageData 数组,包含了 sx, sy, sw, sh 表示的矩形的像素数据。
而且这个数组是 Uint8 类型的,且四位表示一个像素。
我在面试的时候只能想起来这些信息。猜想一下,我们在定义颜色的时候就是使用 rgba(r,g,b,a) 四个维度来表示,而且每个像素值就是用十六位 00-ff 表示,即每个维度的范围是 0~255,即 2^8 位,即 1 byte, 也就是 Uint8 能表示的范围。
所以 100 * 100 canvas 占的内存是 100 * 100 * 4 bytes = 40,000 bytes。
这里的答案并不一定准确。
有同学指出,alpha 不是 0-100 么?我起初也有这样的疑问,不过这篇文章中 https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas 说
The data property returns a Uint8ClampedArray which can be accessed to look at the raw pixel data; each pixel is represented by four one-byte values (red, green, blue, and alpha, in that order; that is, "RGBA" format). Each color component is represented by an integer between 0 and 255.
也就是说即便是 alpha 也是 0-255
那么如何表示 alpha 呢?
接下来这段代码中
可以看出,只需要用 0-255 表示 0-100 就可以啦~
CanvasRenderingContext2D.getImageData() 返回一个ImageData对象,用来描述canvas区域隐含的像素数据,这个区域通过矩形表示,起始点为(sx, sy)、宽为sw、高为sh。
Uint8ClampedArray 描述了一个一维数组,包含以 RGBA 顺序的数据,数据使用 0 至 255(包含)的整数表示。
送给产品经理一段代码 让他放到 console 去
(!(~+[]) + {})[--[~+""][+[]] * [~+[]] + ~~!+[]] + ({} + [])[[~!+[]] * ~+[]]
其实这段代码是我的同事发给我的, 我定睛一看一定有坑, 于是准备破解一番
其实这里面涉及到的知识点无非三个
分析之前我推荐大家看几篇文章
首先我们把代码进行拆分
(!(~+[]) + {})[--[~+""][+[]] * [~+[]] + ~~!+[]]
+
({} + [])[[~!+[]] * ~+[]]
简单来看就是
(A)[B] + (C)[D]
首先来看 A !(~+[]) + {}
+[] -(数据类型转换)-> 0
~+[] --> ~0 -(位运算)-> -1
!(~+[]) --> !(-1) -(类型转换)-> false
(!(~+[]) + {}) --> false + {} -(类型转换)-> 'false[object Object]'
再来看 B --[~+""][+[]]*[~+[]] + ~~!+[]
[~+""] --> [~0] --> [-1]
+[] --> 0
--[~+""][+[]] --> --[-1][0] --> --(-1) --> -2
[~+[]] --> [~0] --> [-1]
~~!+[] --> ~~!0 --> ~~true --> ~-2 -> 1
B --> -2 * [-1] + 1 --> 2 + 1 --> 3
那么 (A)[B] --> 'false[object Object]'[3] --> 's'
再来看 C ({} + [])
这里的 {}
其实是个代码块
所以等价于 +[]
即 [object Object]
再来看 D [~!+[]] * ~+[]
[~!+[]] --> [~!0] --> [!1] --> -2
~+[] --> ~0 --> -1
D -> -2 * -1 --> 2
所以 (C)[D] --> '[object Object]'[2] --> 'b'
那么
(!(~+[]) + {})[--[~+""][+[]] * [~+[]] + ~~!+[]] + ({} + [])[[~!+[]] * ~+[]]
--> (A)[B] + (C)[D)
--> 's' + 'b'
--> 'sb'
谢谢观看 😄
现在可以发给产品经理了
让他打开 chrome, (windows: F12, mac: command+shift+j)
粘贴 (!(~+[])+{})[--[~+""][+[]]*[~+[]] + ~~!+[]]+({}+[])[[~!+[]]*~+[]]
这就是我们前端一直想对你说的话
参考文章
注 文中对产品经理的冒犯仅为了行文效果 😅 前端和产品是相亲相爱的一家人 😆
[1 < 2 < 3, 3 < 2 < 1]
这个题也还可以.
这个题会让人误以为是 2 > 1 && 2 < 3
其实不是的.
这个题等价于
1 < 2 => true;
true < 3 => 1 < 3 => true;
3 < 2 => false;
false < 1 => 0 < 1 => true;
答案是 [true, true]
// the most classic wtf
2 == [[[2]]]
这个题我是猜的. 我猜的 true
, 至于为什么.....
both objects get converted to strings and in both cases the resulting string is "2"
我不能信服...
3.toString()
3..toString()
3...toString()
这个题也挺逗, 我做对了 :) 答案是 error, '3', error
你如果换一个写法就更费解了
var a = 3;
a.toString()
这个答案就是 '3'
;
为啥呢?
因为在 js 中 1.1
, 1.
, .1
都是合法的数字. 那么在解析 3.toString
的时候这个 .
到底是属于这个数字还是函数调用呢? 只能是数字, 因为3.
合法啊!
(function(){
var x = y = 1;
})();
console.log(y);
console.log(x);
答案是 1, error
y 被赋值到全局. x 是局部变量. 所以打印 x 的时候会报 ReferenceError
var a = /123/,
b = /123/;
a == b
a === b
即使正则的字面量一致, 他们也不相等.
答案 false, false
var a = [1, 2, 3],
b = [1, 2, 3],
c = [1, 2, 4]
a == b
a === b
a > c
a < c
字面量相等的数组也不相等.
数组在比较大小的时候按照字典序比较
答案 false, false, false, true
var a = {}, b = Object.prototype;
[a.prototype === b, Object.getPrototypeOf(a) === b]
知识点:
只有 Function 拥有一个 prototype 的属性. 所以 a.prototype
为 undefined
.
而 Object.getPrototypeOf(obj)
返回一个具体对象的原型(该对象的内部[[prototype]]
值)
答案 false, true
function f() {}
var a = f.prototype, b = Object.getPrototypeOf(f);
a === b
f.prototype is the object that will become the parent of any objects created with new f while Object.getPrototypeOf returns the parent in the inheritance hierarchy.
f.prototype 是使用使用 new 创建的 f 实例的原型. 而 Object.getPrototypeOf 是 f 函数的原型.
请看:
a === Object.getPrototypeOf(new f()) // true
b === Function.prototype // true
答案 false
function foo() { }
var oldName = foo.name;
foo.name = "bar";
[oldName, foo.name]
答案 ['foo', 'foo']
知识点:
因为函数的名字不可变.
"1 2 3".replace(/\d/g, parseInt)
知识点:
str.replace(regexp|substr, newSubStr|function)
如果replace函数传入的第二个参数是函数, 那么这个函数将接受如下参数
由于题目中的正则没有分组, 所以等价于问
parseInt('1', 0)
parseInt('2', 2)
parseInt('3', 4)
答案: 1, NaN, 3
function f() {}
var parent = Object.getPrototypeOf(f);
f.name // ?
parent.name // ?
typeof eval(f.name) // ?
typeof eval(parent.name) // ?
先说以下答案 'f', 'Empty', 'function', error
这个答案并不重要.....
这里第一小问和第三小问很简单不解释了.
第二小问笔者在自己的浏览器测试的时候是 ''
, 第四问是 'undefined'
所以应该是平台相关的. 这里明白 parent === Function.prototype
就好了.
var lowerCaseOnly = /^[a-z]+$/;
[lowerCaseOnly.test(null), lowerCaseOnly.test()]
知识点:
这里 test 函数会将参数转为字符串. 'nul'
, 'undefined'
自然都是全小写了
答案: true, true
[,,,].join(", ")
[,,,] => [undefined × 3]
因为javascript 在定义数组的时候允许最后一个元素后跟一个,
, 所以这是个长度为三的稀疏数组(这是长度为三, 并没有 0, 1, 2三个属性哦)
答案: ", , "
var a = {class: "Animal", name: 'Fido'};
a.class
这个题比较流氓.. 因为是浏览器相关, class
是个保留字(现在是个关键字了)
所以答案不重要, 重要的是自己在取属性名称的时候尽量避免保留字. 如果使用的话请加引号 a['class']
var a = new Date("epoch")
知识点:
简单来说, 如果调用 Date 的构造函数传入一个字符串的话需要符合规范, 即满足 Date.parse 的条件.
另外需要注意的是 如果格式错误 构造函数返回的仍是一个Date 的实例 Invalid Date
.
答案 Invalid Date
var a = Function.length,
b = new Function().length
a === b
我们知道一个function(Function 的实例)的 length
属性就是函数签名的参数个数, 所以 b.length == 0.
另外 Function.length 定义为1......
所以不相等.......答案 false
var a = Date(0);
var b = new Date(0);
var c = new Date();
[a === b, b === c, a === c]
还是关于Date 的题, 需要注意的是
答案 false, false, false
var min = Math.min(), max = Math.max()
min < max
知识点:
有趣的是, Math.min 不传参数返回 Infinity
, Math.max 不传参数返回 -Infinity
😆
答案: false
function captureOne(re, str) {
var match = re.exec(str);
return match && match[1];
}
var numRe = /num=(\d+)/ig,
wordRe = /word=(\w+)/i,
a1 = captureOne(numRe, "num=1"),
a2 = captureOne(wordRe, "word=1"),
a3 = captureOne(numRe, "NUM=2"),
a4 = captureOne(wordRe, "WORD=2");
[a1 === a2, a3 === a4]
知识点:
通俗的讲
因为第一个正则有一个 g 选项 它会‘记忆’他所匹配的内容, 等匹配后他会从上次匹配的索引继续, 而第二个正则不会
举个例子
var myRe = /ab*/g;
var str = 'abbcdefabh';
var myArray;
while ((myArray = myRe.exec(str)) !== null) {
var msg = 'Found ' + myArray[0] + '. ';
msg += 'Next match starts at ' + myRe.lastIndex;
console.log(msg);
}
// Found abb. Next match starts at 3
// Found ab. Next match starts at 9
所以 a1 = '1'; a2 = '1'; a3 = null; a4 = '2'
答案 [true, false]
var a = new Date("2014-03-19"),
b = new Date(2014, 03, 19);
[a.getDay() === b.getDay(), a.getMonth() === b.getMonth()]
这个....
JavaScript inherits 40 years old design from C: days are 1-indexed in C's struct tm, but months are 0 indexed. In addition to that, getDay returns the 0-indexed day of the week, to get the 1-indexed day of the month you have to use getDate, which doesn't return a Date object.
a.getDay()
3
b.getDay()
6
a.getMonth()
2
b.getMonth()
3
都是套路!
答案 [false, false]
if ('http://giftwrapped.com/picture.jpg'.match('.gif')) {
'a gif file'
} else {
'not a gif file'
}
知识点:
String.prototype.match 接受一个正则, 如果不是, 按照 new RegExp(obj)
转化. 所以 .
并不会转义
那么 /gif
就匹配了 /.gif/
答案: 'a gif file'
function foo(a) {
var a;
return a;
}
function bar(a) {
var a = 'bye';
return a;
}
[foo('hello'), bar('hello')]
在两个函数里, a作为参数其实已经声明了, 所以 var a; var a = 'bye'
其实就是 a; a ='bye'
所以答案 'hello', 'bye'
全部结束!
由于笔者水平有限, 如果解释有误, 还望指出 😄
通过整理, 笔者发现绝大部分题目都是因为自己对于基础知识或者说某个 API 的参数理解偏差才做错的.
笔者的重灾区在原型那一块, 所以这次被虐和整理还是很有意义呀.
笔者相信 坚实的基础是深入编程的前提. 所以基础书还是要常看啊 🐹
最后这些变态题现在看看还变态嘛?
事情的起源是这样的, 同事发给我两段代码, 如下:
var a = [1, 2, 3, 1, 2, 3];
a.forEach((item, index) => {
console.log(index, item);
if (item === 1) {
a.splice(index, 1);
}
});
// 输出
// 0 1
// 1 3
// 2 1
// 3 3
var a = [1, 2, 3, 1, 2, 3];
a.forEach((item, index) => {
console.log(index, item);
if (item === 1) {
a.push(1);
}
});
// 输出
// 0 1
// 1 2
// 2 3
// 3 1
// 4 2
// 5 3
为什么第一个输出四次, 第二个不输出8次呢?
其实这样的事情在我们平常写代码的时候也经常发生,
如果这个改成 for 循环, 或许完全不一样. 那么 forEach
的 callback
到底执行了多少次呢?
这样的事情当然要看规范了, Array.prototype.forEach() 中文
forEach 方法按升序为数组中含有效值的每一项执行一次callback 函数,那些已删除(使用delete方法等情况)或者未初始化的项将被跳过(但不包括那些值为 undefined 的项)(例如在稀疏数组上)。
forEach 遍历的范围在第一次调用 callback 前就会确定。调用forEach 后添加到数组中的项不会被 callback 访问到。如果已经存在的值被改变,则传递给 callback 的值是 forEach 遍历到他们那一刻的值。已删除的项不会被遍历到。如果已访问的元素在迭代时被删除了(例如使用 shift()) ,之后的元素将被跳过
这里面感觉最重要的是:
看不懂? show me the code
// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
// 如果 Array.prototype.forEach 没有定义的话
if (!Array.prototype.forEach) {
Array.prototype.forEach = function (callback/*, thisArg*/) {
// T 为 callback 的指向, 如果指定的话, 看 step-5
// k 为 循环的索引
var T, k;
if (this == null) {
throw new TypeError('this is null or not defined');
}
// 1. Let O be the result of calling toObject() passing the
// |this| value as the argument.
// @see https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
// Object构造函数为给定值创建一个对象包装器。如果给定值是 null 或 undefined,将会创建并返回一个空对象,否则,将返回一个与给定值对应类型的对象。
// 当以非构造函数形式被调用时,Object 等同于 new Object()。
var O = Object(this);
// 2. Let lenValue be the result of calling the Get() internal
// method of O with the argument "length".
// 3. Let len be toUint32(lenValue).
// @see https://stackoverflow.com/questions/8286925/whats-array-length-0-used-for
// 保证 len 为一个小于 2^32 的整数
// 这里需要注意, step-7 的终止条件是 k < len;
// 所以, forEach 遍历的范围在第一次调用 callback 前就会确定
var len = O.length >>> 0;
// 4. If isCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
// 保证 callback 是个函数
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
// 5. If thisArg was supplied, let T be thisArg; else let
// T be undefined.
if (arguments.length > 1) {
T = arguments[1];
}
// 6. Let k be 0.
k = 0;
// 7. Repeat while k < len.
while (k < len) {
// 第 k 项
var kValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator.
// b. Let kPresent be the result of calling the HasProperty
// internal method of O with argument Pk.
// This step can be combined with c.
// c. If kPresent is true, then
// 保证 k 这个索引是 O 的属性
if (k in O) {
// i. Let kValue be the result of calling the Get internal
// method of O with argument Pk.
// 赋值
kValue = O[k];
// ii. Call the Call internal method of callback with T as
// the this value and argument list containing kValue, k, and O.
// 调用 callback, T 为 callback 绑定的 this, 参数分别是 item, index, 和 array 本身
callback.call(T, kValue, k, O);
}
// d. Increase k by 1.
k++;
}
// 8. return undefined.
};
}
刚刚说的两条分别对应
step-3
// 3. Let len be toUint32(lenValue).
var len = O.length >>> 0;
和 step-7-c
if (k in O)
回到第一题
var a = [1, 2, 3, 1, 2, 3];
a.forEach((item, index) => {
console.log(index, item);
if (item === 1) {
a.splice(index, 1);
}
});
1. a = [1,2,3,1,2,3]; len = 6
2. k = 0; console.log(0, 1);
3. splice(0, 1) ---> a = [2,3,1,2,3]
4. k = 1; console.log(1, 3);
5. k = 2; console.log(2, 1);
6. splice(2, 1) ---> a = [2,3,2,3];
7. k = 3; console.log(3, 3);
8. k = 4; k not in a;
9. k = 5; k not in a;
第二题比较简单, 新添加的两个 1 都不会遍历
所以两种情况的 while 循环都是 6 次
但是第一种由于 '4'
'5'
都不在 array 里面, 所以 callback 只执行了 4 次
第二种情况 callback 执行了 6 次
好啦, 你听明白了嘛~
参考资料
易企秀(http://www.eqxiu.com/)在2015年12月, 我入职的时候, 整个项目是一个大型的单页应用. 包括了首页, 作品列表页, 编辑器, 作品详情, 秀场等. 由于业务的急剧膨胀, 大项目组已经无法满足敏捷开发的需要, 产品会变得冗长低效, 产品层面上整个主站需要进行剥离, 工程项目的拆分及重构也势在必行. 易企秀的核心业务是 H5编辑和预览. 由于迭代迅速, 功能往往以实现为主, 缺乏设计, 代码略显混乱. 前端组认为为了未来支持更为丰富的组件和编辑功能, 需要对预览还有编辑器项目进行一次’重构’.
回头看这次决定, 出发点是非常好的, 但是笔者认为重构的安排略显仓促.
一, 项目的拆分, 重构和优化是不应该一起进行的. 三者想要达到的目的各不相同. 而最初的目的是不清晰的, 只是希望完整实现一个更好的版本, 这样势必导致重构人员会发散, 最终的结果是重写了整个项目… 造成修改的代价及其高昂, 而且开发周期会很长, 测试的压力会很大.
二, 项目重构的前提是对项目有了充足的了解, 但是当初无论是文远还是我都对核心业务知之甚少(作为当初资历最浅的两个人且无大型项目重构经验). 所以在重构的整个过程中, 对业务理解所花费的时间非常多, 需要频繁的与同事进行交流, 不可避免的会出现很多理解的偏差, 也造成了很多业务逻辑上的 bug.
不过, 重构的大幕在2016年3月正式开启了.
为了好理解, 先将 H5预览项目称之为 view 项目, 将编辑器项目称之为 editor 项目.
整个重构分成5个阶段
文远在一开始进行核心功能重写时, 选择了 OO 进行重构, 经过讨论, 我们认为 OO 非常适合 H5 多组件的情形. 非常方便未来扩展新的功能. 而且由于 view 项目是一个非常独立的项目, 所以我们希望其不依赖任何框架, 这样才有机会融入到其他框架中去(当时的情况是编辑器是用 ng1.2, 并且当时认为可能会出现手机编辑器等多种 H5编辑器).
在讨论到 jQuery 的时候, 由于项目中大量使用了 jQuery 的选择器等语法也使用了 jQuery 的相关插件, 从开发工作量上考虑, 我们决定保留.
在是否使用 ES6 语法上, 团队产生了较大分歧, 笔者当时更为激进, 希望采用 typescript 来进行编写, 理由是 view 项目将作为种子项目, 为未来的多编辑器进行服务, 希望保证质量, 团队其他成员认为 ts 或者 es6 的使用增加了学习成本, 最后妥协的结果是我们选择了部分 es6 语法. 对于这个妥协, 我个人是满意的, 事实证明, es6 在当前前端开发中大行其道, 应该会在未来的开发中成为默认语言.
由于当初笔者正好对项目组分享了 gulp4.0(http://slides.com/xy2/gulp-1#/) 的使用, (当初项目组还在使用 grunt 进行构建), 所以我们采用了 gulp4.0 作为构建工具.
我们决定将 less 转化成 sass.
在是否使用模块化开发问题上, 团队也发生了分歧, 由于当初没有人有模块化开发的经验, 所以模块化开发的提议被否决了, 一个理由是当初的方案就是采用挂载全局变量的方式, 比较好理解, 不过当初全局变量非常随意混乱. 最后的结果是 项目使用命名空间的形式, 将所有的组件都按照功能挂载到 EQX 这样一个全局变量上. 最终形成了 EQX.init, EQX.util, EQX.const, EQX.API, EQX.HOST, EQX.tpl, EQX.effect 等子模块, 场景类, 组件类, 辅助类等则直接挂载到 EQX上.
(上图为EQX全局变量的部分属性)
我们设计了大量的辅助类来解决复杂问题, 例如 EQX.formManager(处理表单提交逻辑), commentManager(处理表单逻辑), bgm(处理背景音乐), pageScroll(处理翻页逻辑), progressbar(处理进度条逻辑) 等
除此之外,
为了避免在业务逻辑中兼容各式各样的老数据, 我们增加了一层数据改造层(adapter), 来集中对发现的老数据, 坏数据做一次转换(但是因为对项目本身了解不深, 在改造数据时, 无法预计到所有的数据结构, 所以导致改造逻辑简单, 无法真正做到所有数据的兼容, 甚至改造错误), 减轻业务逻辑.
我们统一了异步处理的方式: 全部使用 Promise, 原则上不再使用回调函数.
我们规范了初始化场景的逻辑, 分为以下步骤
对整个 view 项目中出现的业务概念我们做了如下抽象.
EqxScene(场景)
EqxPage(页面), EqxLongPage(长页), EqxXiuPage(秀版, 广告页)…
EqxBackground(背景)
EqxBgm(背景音乐)
EqxComp(组件)
EqxText, EqxImage, EqxInput, EqxInputPhone...
EqxPageEffect(页面特效)
EqxSnowEffect ...
EqxPageScroll(翻页器)
book, card ... (采用策略模式, 抽象出不同的翻页算法)
EqxProgressBar(进度条)
EqxManager(辅助类)
EqxEventManager, EqxSoundManager, EqxStatManager, EqxCommentManager, EqxFormManager
EqxAgent
EqxSoundAgent…
大致关系图如下.
按照这样的规划, 我们完成了第一个里程碑, 实现了现有功能的全部改造.
由于 view 项目是公司最核心的项目, 我们经过广泛讨论确定了逐步灰度的方案: 按照场景发布时间进行划分逐步进行灰度, 且灰度策略随时进行调整.
在历时一个月的时间内, 我们累计处理了几百个 bug. 这也反映出对业务理解不深. 不过感谢测试团队, 也感谢我们的项目管理方式(jira), 可以让我们有序的进行 bug 修改.
在灰度进行中期, 笔者开始了第三阶段的开发, H5编辑器的拆分 和 view 项目的模块化改造, 升级成 core (核心组件库) 项目
笔者在初期定下了如下重构理念
经过项目拆分, 笔者划定了项目的整个边界
包括 模板, 素材, 页面管理, 编辑, 场景设置, 设计师功能等.
其中涉及到编辑组件的功能边界如下
在第四个阶段, 笔者负责了搭建整个项目框架 (保证项目可以最小化运行 + 核心编辑能力) 基本框架如下
随后, 我们按照模块化的方式进行了项目划分和开发. 一个典型的模块及子模块(样式编辑-触发)的代码组织如下
经过近两月的努力, 我们小组(四人)完成了编辑器的重构工作, 随后进行了第二次灰度和 bug 修改(又是上百个 bug…..), 这次我们采用了用户注册时间和权限的策略进行灰度.
在这段时间, 组内成员进行了 code review 实践, 效果比较好. 特此记录.
笔者认为 code review 是一个集合了知晓同伴工作, 协调工作进度, 讲解代码, 提高团队成员水平, 统一风格等........诸多好处的软件开发辅助活动...... code review 是一种形式, 绝不是目的.
在实践中, 我们每天至少花一小时的时间进行代码交流. 鼓励团队成员每天进行有意义的代码提交 (至少1-2次), 这样可以及时的 review, 及时的给出建议, 逐步改善编码习惯. 具体在 code review 进行过程中
毕竟团队开发, 大家好才是真的好 :D
现在重构已经到一段落. 不过现在仍然存在 view, core, editor 三个项目. 目前 core 项目已经非常稳定的工作, 提供渲染和编辑场景的内部逻辑. core 项目目前支持 PC 编辑器, 广告项目场景渲染 以及 安卓混合模式的开发.
下一阶段需要将 core, view 进行代码的合并形成真正意义上的 核心组件库.
经过这次历时接近一年的’重构’, 笔者认为既有良好的实践, 也有诸多可以改进的地方.
当然, 总结重构, 我们也有很多不足:
对于最后的重构结果笔者是较为满意的, 运行了近两个月的时间, 在新需求的开发过程中, 由于前期的顶层设计, 每一个组件都’理所当然’有他的位置, 整个架构很好的支撑了业务的发展. 笔者希望, 未来的重构最好不要伤筋动骨而应该是渐进增强. 总之, 感谢这次经历 :D
两者都会并行下载,不会影响页面的解析。
defer 会按照顺序在 DOMContentLoaded 前按照页面出现顺序依次执行。
async 则是下载完立即执行。
先来看一个普通的 script 标签。
<script src="a.js"></script>
浏览器会做如下处理
<script src="d.js" defer></script>
<script src="e.js" defer></script>
DOMContentLoaded
事件前 依次执行 d.js, e.js。<script src="b.js" async></script>
<script src="c.js" async></script>
DOMContentLoaded
事件前或者后 )A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.