I'm a Frontend Developer in Shanghai/China
wusb / blog Goto Github PK
View Code? Open in Web Editor NEW🖋 Personal blog, Welcome Fork, Watch & Star
Home Page: http://simbawu.com
License: MIT License
🖋 Personal blog, Welcome Fork, Watch & Star
Home Page: http://simbawu.com
License: MIT License
在搜索功能开发的时候,有时会碰到这种需求:点击输入法上的搜索按钮进行关键词搜索。这个需求可以拆分成两个需求:
输入法有“搜索”按钮;
点击“搜索”按钮执行搜索事件。
第一个需求很简单,设置input
的type="search"
就可以。
<input type="search" placeholder="搜索关键词" />
第二个需求,可能很少接触,这个时候就需要借用form
表单的submit
提交。
<form onsubmit="handleSubmit()">
<input type="search" placeholder="搜索关键词" />
</form>
如果不加处理,就会触发form
表单submit
默认的页面刷新事件。我们必须手动消除form
表单submit
事件的页面默认刷新行为。下面推荐三种写法:
return false
<form onsubmit="handleSubmit();return false">
<input type="search">
</form>
function handleSubmit() {
...
}
return false
<form onsubmit="handleSubmit()">
<input type="search">
</form>
function handleSubmit() {
...
return false
}
preventDefault
<form onsubmit="handleSubmit(event)">
<input type="search">
</form>
function handleSubmit(event) {
e.preventDefault(event);
...
}
1、2两种写法在React均不支持,只能采用preventDefault
了,写法如下:
handleSubmit(event){
event.preventDefault();
...
}
render(){
return (
<form onSubmit={this.handleSubmit}>
<input type="search" placeholder="搜索关键词" />
</form>
)
}
不过,有个细节不知道大家注意到没,上面第三种写法的handleSubmit
在onsubmit
里显示的传递了event
,而这里并没有。是我多此一举还是有所考虑?大家思考下,我下次再说。
在说深拷贝与浅拷贝前,我们先看两个简单的案例:
//案例1
var num1 = 1, num2 = num1;
console.log(num1) //1
console.log(num2) //1
num2 = 2; //修改num2
console.log(num1) //1
console.log(num2) //2
//案例2
var obj1 = {x: 1, y: 2}, obj2 = obj1;
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}
obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 2, y: 2}
console.log(obj2) //{x: 2, y: 2}
按照常规思维,obj1
应该和num1
一样,不会因为另外一个值的改变而改变,而这里的obj1
却随着obj2
的改变而改变了。同样是变量,为什么表现不一样呢?这就要引入JS中基本类型和引用类型的概念了。
ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是那些保存在栈内存中的简单数据段,即这种值完全保存在内存中的一个位置。而引用类型值是指那些保存堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保存对象。
打个比方,基本类型和引用类型在赋值上的区别可以按“连锁店”和“单店”来理解:基本类型赋值等于在一个新的地方安装连锁店的规范标准新开一个分店,新开的店与其他旧店互不相关,各自运营;而引用类型赋值相当于一个店有两把钥匙,交给两个老板同时管理,两个老板的行为都有可能对一间店的运营造成影响。
上面清晰明了的介绍了基本类型和引用类型的定义和区别。目前基本类型有:Boolean、Null、Undefined、Number、String、Symbol,引用类型有:Object、Array、Function。之所以说“目前”,因为Symbol就是ES6才出来的,之后也可能会有新的类型出来。
再回到前面的案例,案例1中的值为基本类型,案例2中的值为引用类型。案例2中的赋值就是典型的浅拷贝,并且深拷贝与浅拷贝的概念只存在于引用类型。
既然已经知道了深拷贝与浅拷贝的来由,那么该如何实现深拷贝?我们先分别看看Array和Object自有方法是否支持:
var arr1 = [1, 2], arr2 = arr1.slice();
console.log(arr1); //[1, 2]
console.log(arr2); //[1, 2]
arr2[0] = 3; //修改arr2
console.log(arr1); //[1, 2]
console.log(arr2); //[3, 2]
此时,arr2
的修改并没有影响到arr1
,看来深拷贝的实现并没有那么难嘛。我们把arr1改成二维数组再来看看:
var arr1 = [1, 2, [3, 4]], arr2 = arr1.slice();
console.log(arr1); //[1, 2, [3, 4]]
console.log(arr2); //[1, 2, [3, 4]]
arr2[2][1] = 5;
console.log(arr1); //[1, 2, [3, 5]]
console.log(arr2); //[1, 2, [3, 5]]
咦,arr2
又改变了arr1
,看来slice()只能实现一维数组的深拷贝。
具备同等特性的还有:concat、Array.from() 。
var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}
obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 2, y: 2}
var obj1 = {
x: 1,
y: {
m: 1
}
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 2}}
console.log(obj2) //{x: 2, y: {m: 2}}
经测试,Object.assign()也只能实现一维对象的深拷贝。
var obj1 = {
x: 1,
y: {
m: 1
}
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 2, y: {m: 2}}
JSON.parse(JSON.stringify(obj))
看起来很不错,不过MDN文档 的描述有句话写的很清楚:
undefined、
任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成null
(出现在数组中时)。
我们再来把obj1
改造下:
var obj1 = {
x: 1,
y: undefined,
z: function add(z1, z2) {
return z1 + z2
},
a: Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(JSON.stringify(obj1)); //{"x":1}
console.log(obj2) //{x: 1}
发现,在将obj1进行JSON.stringify()
序列化的过程中,y、z、a都被忽略了,也就验证了MDN文档的描述。既然这样,那JSON.parse(JSON.stringify(obj))
的使用也是有局限性的,不能深拷贝含有undefined、function、symbol值的对象,不过JSON.parse(JSON.stringify(obj))
简单粗暴,已经满足90%的使用场景了。
经过验证,我们发现JS 提供的自有方法并不能彻底解决Array、Object的深拷贝问题。只能祭出大杀器:递归
function deepCopy(obj) {
// 创建一个新对象
let result = {}
let keys = Object.keys(obj),
key = null,
temp = null;
for (let i = 0; i < keys.length; i++) {
key = keys[i];
temp = obj[key];
// 如果字段的值也是一个对象则递归操作
if (temp && typeof temp === 'object') {
result[key] = deepCopy(temp);
} else {
// 否则直接赋值给新对象
result[key] = temp;
}
}
return result;
}
var obj1 = {
x: {
m: 1
},
y: undefined,
z: function add(z1, z2) {
return z1 + z2
},
a: Symbol("foo")
};
var obj2 = deepCopy(obj1);
obj2.x.m = 2;
console.log(obj1); //{x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(obj2); //{x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)}
可以看到,递归完美的解决了前面遗留的所有问题,我们也可以用第三方库:jquery的$.extend
和lodash的_.cloneDeep
来解决深拷贝。上面虽然是用Object验证,但对于Array也同样适用,因为Array也是特殊的Object。
到这里,深拷贝问题基本可以告一段落了。但是,还有一个非常特殊的场景:
循环引用拷贝
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);
此时如果调用刚才的deepCopy函数的话,会陷入一个循环的递归过程,从而导致爆栈。jquery的$.extend
也没有解决。解决这个问题也非常简单,只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可,修改一下代码:
function deepCopy(obj, parent = null) {
// 创建一个新对象
let result = {};
let keys = Object.keys(obj),
key = null,
temp= null,
_parent = parent;
// 该字段有父级则需要追溯该字段的父级
while (_parent) {
// 如果该字段引用了它的父级则为循环引用
if (_parent.originalParent === obj) {
// 循环引用直接返回同级的新对象
return _parent.currentParent;
}
_parent = _parent.parent;
}
for (let i = 0; i < keys.length; i++) {
key = keys[i];
temp= obj[key];
// 如果字段的值也是一个对象
if (temp && typeof temp=== 'object') {
// 递归执行深拷贝 将同级的待拷贝对象与新对象传递给 parent 方便追溯循环引用
result[key] = DeepCopy(temp, {
originalParent: obj,
currentParent: result,
parent: parent
});
} else {
result[key] = temp;
}
}
return result;
}
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);
console.log(obj1); //太长了去浏览器试一下吧~
console.log(obj2); //太长了去浏览器试一下吧~
至此,已完成一个支持循环引用的深拷贝函数。当然,也可以使用lodash的_.cloneDeep
噢~。
文章同步于以下社区,可以选一个关注我噢 。◕‿◕。
作为一个程序员,搭建一个个人博客几乎是所有人的需求,一来比较酷,二来也可以记录自己的学习和生活总结。但如果你不是全栈工程师,实现这个需求还是有点麻烦。后端搭建一套现有的前端框架及前端写API都还是有一定门槛的,当然,如果你是大牛当我没说,哈哈哈!
下面,我将介绍一个特别简单的方法,甚至不用写代码,执行几个命令就可以搭建一个博客,就算你不是程序员,也是So easy。那就是:fork我的博客。为什么说fork我的博客就可以搭建一个博客呢?博客重要的是有内容,并且可以随时更新,而不是一个静态页。这就要用到本文的核心:GitHub GraphQL API,这是github提供的一个开放式的API。我们只需要将文章用Markdown写好后,放到博客项目Issues里面,然后通过这个api,获取我们的写的文章,再前端渲染,就可以啦!!是不是特别棒,都不要写API,也不用考虑文章存哪。下面我来介绍如何实现:
请求GitHub GraphQL API,首先需要按照以下步骤在github申请一个access token:
右上角个人头像 > Settings > Developer settings > Personal access tokens > Generate new token
然后在Token description 写好关于这个token的描述,在Select scopes选择相应的权限,这里只需要user > read:user 就可以,点击Generate token按钮后会跳转到token列表页,这时需要马上把这个token记录下来,因为这是敏感数据,刷新后就没有了,不然得重新申请。
建议大家直接Fork我的项目 simbawus/blog,再修改相应配置,这样可以免去开发的成本,并且这个项目会持续更新,配置修改及启动可查看我项目的README。当然也可以fork后进行二次开发。也十分鼓励大家从零开始开发,也顺便练练手。
关于GraphQL的介绍,可查看我些的这篇文章前端应该知道的GraphQL。
GitHub GraphQL API的文档并没有使用示例,如果之前没用过GraphQL API,还是有点懵的,下面我举三个常见的例子说明下,具体可以看我博客代码,别忘了Star噢~。
通常,我们会在博客首页设计一个有分类的文章列表,这就要求在发布Issue时需要选择对应的label。先看官方label文档:
Connections 里面有issues,所以在查询labels的同时,还可以查询issues。先列出要传输的数据data,核心也在这:
data = {
query: `query {
repository(owner:"simbawus", name: "blog") {
issues(orderBy:{field: UPDATED_AT, direction: DESC} , labels: null, first: 10, after: ${page}) {
edges{
cursor
node{
title
updatedAt
bodyText
number
}
}
}
labels(first: 100){
nodes{
name
}
}
}
}`
};
repository
代表查询指定的仓库,括号里的参数owner代表这个仓库的所有者,name代表仓库名称。issues
表示要查询的issue列表,里面的参数表示这个列表的条件:orderBy
为排序方式,根据更新时间UPDATED_AT和倒序DESC来,labels
为null,说明查询的是所有issues,first
表示一次查询返回的issues数量,after
传上一个issue的id,可用来分页,最终这次请求拿到的数据如下:
{
"data": {
"repository": {
"issues": {
"edges": {
"0": {
"cursor": "Y3Vyc29yOnYyOpK5MjAxOC0wNC0yNlQxMDoyNjoxNiswODowMM4S8hYL",
"node": {
"bodyText": "作为一个程序员...",
"number": "11",
"title": "如何利用GitHub GraphQL API开发个人博客?",
"updatedAt": "2018-04-22T03:46:34Z",
}
}
}
},
"labels": {
"nodes": {
"0": {
"name": "JavaScript"
}
}
}
}
}
}
search这个connections的文档写的让我一脸懵逼,摸索了好久才写出来,大家可以试着按官网文档写一下。
let data = {
query: `query {
search(query:"${keyWords} repo:simbawus/blog", type: ISSUE, first: 10) {
issueCount
edges{
cursor
node{
... on Issue {
title
number
bodyText
updatedAt
}
}
}
}
}`
};
search的query参数类型为String!,表示一个非空的字符串,怎么也想不到要这么写才行吧?query:"${keyWords} repo:simbawus/blog"
。node 这个fields的文档,看的也是二脸懵逼,还好想到es6的扩展符。
最重要的是文章内容这部分了,传输数据比较简单:
let data = {
query: `query {
repository(owner:"simbawus", name: "blog") {
issue(number: ${articleId}) {
title
updatedAt
bodyHTML
}
}
}`
};
请求直接返回一段HTML,问题是如何处理这段HTML,格式化并且高亮文章里面的代码。这里我用的是React的dangerouslySetInnerHTML
和github的css库github-markdown-css
,核心代码如下:
import 'github-markdown-css';
class ArticlePage extends React.Component {
_renderHTML() {
return { __html: this.state.bodyHTML };
}
render() {
return (
<div className = 'markdown-body'
dangerouslySetInnerHTML = { this._renderHTML() } >
</div>
);
}
}
结合GitHub GraphQL API开发个人博客的核心内容基本就这么多了,具体代码欢迎查看github:simbawus/blog,一起踩坑。
文章同步于以下社区,可以选一个关注我噢 。◕‿◕。
ERROR in multi (webpack)-dev-server/client?http://0.0.0.0:8097 webpack/hot/dev-server Chrome
Module not found: Error: Can't resolve 'Chrome' in 'C:\Program Files\Git\blog'
@ multi (webpack)-dev-server/client?http://0.0.0.0:8097 webpack/hot/dev-server Chrome
(这篇文章以React为例,其他框架类同。)
如何发布一个NPM包
package.json
文件的name
换成你确定的名字,例如我作为案例的组件名:react-components-calendar
。根据我Demo里面的目录结构,建议先在components
目录开发我们的组件,然后在page
目录进行引用。
配置webpack.config.js
:
entry: {
app: ['./src/main.js'],
component: ['./components/index.js']
},
output: {
path: path.resolve(__dirname, '../build'),
filename: '[name].js',
publicPath: "/build/",
libraryTarget: 'umd',
library: 'Calendar'
},
externals: {
'react': 'react',
'react-dom': 'react-dom'
}
在Demo的基础上,主要做了三项改进:
入口entry
项新增了component
这一项目,用来作为我们开发的组件的入口。
输出output
项filename文件名采用[name]获取entry
的键名作为文件名。libraryTarget为确定你打包后的文件,模块组织是遵循的什么规范,可选项有:
"var" - Export by setting a variable: var Library = xxx (default)
"this" - Export by setting a property of this: this["Library"] = xxx
"commonjs" - Export by setting a property of exports: exports["Library"] = xxx
"commonjs2" - Export by setting module.exports: module.exports = xxx
"amd" - Export to AMD (optionally named - set the name via the library option)
"umd" - Export to AMD, CommonJS2 or as property in root
一般采用通用类型umd。library用来指定你使用require时的模块名。
扩展externals
,这项配置不处理的某些以依赖库。比如,我们的组件是基于React的,如果我们的组件也是用在React项目里,那组件依赖的React就不需要进行babel转义了。如果转义,那样打出来的包就会特别大,没必要。不过在测试开发阶段,这项应该注释掉,在打包发布之前加上就可以。
配置package.json
:
"main": "build/component.js"
只需要在main这项定义打包后的组件的位置就OK了。
执行yarn build
打包后,在build目录会生成app.js文件和component.js文件。但是我们这里只需要component.js文件,因为我没有对webpack.config.js做环境的区分,所以会将entry
项的文件都打包出来。现在我们webpack配置比较简单,所以需要手动处理,将app.js删除后再提交。
现在开始发布
#先登录NPM账号:
npm login
#会依次让你输入用户名、密码、和邮箱
Username: simbawu
Password:
Email: (this IS public) [email protected]
#登录成功会出现以下提示信息:
Logged in as simbawu on https://registry.npmjs.org/.
#执行发布命令:
npm publish
#发布成功后会出现以下提示信息:
+ [email protected]
#这里react-components-calendar是我的NPM包名,0.0.7是包的版本号
接下来,我们可以在NPM官网,通过搜索包名或者在个人中心看到刚刚发布的包。
安装刚刚发布的包
yarn add react-components-calendar --dev
安装成功后,在之前我们引入开发的组件的地方将引用路径替换为包引用
// import Calendar from '../../../components/index.js'
import Calendar from 'react-components-calendar'
注释webpack.config.js中的externals
这项
output: {
path: path.resolve(__dirname, '../build'),
filename: '[name].js',
publicPath: "/build/",
libraryTarget: 'umd',
library: 'Calendar'
},
// externals: {
// 'react': 'react',
// 'react-dom': 'react-dom'
// },
启动项目,查看效果。
yarn start
最后,祝贺大功告成!赶紧去发自己的第一个包吧~
附上本项目的源码:react-components-calendar。
(此总结只保证对基础库1.9.91有效)
onCanplay在真机及开发工具上均无法触发,因此无法在此生命周期执行相关操作
onPlay开始播放音频,我们通常会考虑在这个生命周期内获取音频的长度duration,但是回调函数内并不能实时获取,感觉违背了常识。可以采用setTimeout设定一个延时来获取,但也做不到实时,建议在 onTimeUpdate回调内获取我们需要的duration。
在此生命周期,我们希望能获取音频的播放状态paused,来做一些逻辑上的处理,但并不能准备获取正确的值true,此处需要手动设置。比如:
const audio = wx.getBackgroundAudioManager();
audio.onPause(() => {
//此时audio.paused,按道理来说是true,并不一定是true
self.setData({
pause: audio.paused
})
//所以需要显式的去设置
self.setData({
pause: true
})
})
文档写的参数是postion,如果是首次接触很容易误导,其实参数就是一个Number,比如:
const audio = wx.getBackgroundAudioManager();
audio.seek(500);
如果不设置的话,在iOS设备上会导致音频无法播放,Android上影响不大。
const audio = wx.getBackgroundAudioManager();
此audio
获取是一个全局的音频对象,如果一个页面里面定义多个audio
,它们的生命周期是共享的。比如说,我们在同一个page的两个function
内均定义了audio
,并且,都使用了onTimeUpdate
这个生命周期,那么这两个onTimeUpdate
是共享的,并不会同时存在,只会以最近一次调用的audio
为准。
ECharts是众所周知的一个百度出品的数据可视化图形框架,前两周在跟阿里健康合作的项目体重曲线正好有用到,借助ECharts的定制化开发,可高度还原产品及设计要求。
跟直接引用ECharts不同的是,我们可以NPM安装好后,按需引入:
1.安装ECharts
yarn add echarts --dev
2.引入需要的模块
import echarts from 'echarts/lib/echarts';
import 'echarts/lib/chart/line';
这个模块只需要绘制曲线图,所以只引入折线图,这样打包的时候可以节省空间。
3.定义好绘制体重曲线的函数
drawWeightTrend(data){
let xAxisDate = data.map((item)=>{
return item.record_on;
});
let yAxisDate = data.map((item)=>{
return item.weight;
});
xAxisDate = xAxisDate.reverse().slice(-7);
yAxisDate = yAxisDate.reverse().slice(-7);
let weightTrend = echarts.init(document.getElementById('weightTrend'));
weightTrend.setOption({
grid:{
left: 30,
top: 20,
right: 30,
bottom: 20
},
xAxis: [{
type: 'category',
boundaryGap: false,
data: xAxisDate,
axisLine: {
show: false
},
axisTick:{
length: 0
},
axisLabel: {
color: '#999'
},
splitLine: {
lineStyle:{
color: '#eee'
}
}
}],
yAxis: [{
type: 'value',
axisLine: {
show: false
},
axisTick:{
length: 0
},
axisLabel: {
color: '#999'
},
splitLine: {
lineStyle:{
color: '#eee'
}
},
min:25
}],
series: [{
type: 'line',
label: {
normal:{
show: true,
color: '#3acfb9',
borderColor: '#3acfb9'
}
},
itemStyle: {
normal:{
color: '#3acfb9',
borderWidth: 2,
}
},
lineStyle: {
normal: {
color: '#3acfb9'
}
},
areaStyle: {
normal: {
color: '#3acfb9',
opacity: 0.4
}
},
showAllSymbol: true,
symbol: 'emptyCircle',
symbolSize: 6,
data: yAxisDate
}]
})
}
函数的setOption配置跟其他使用方式一致,可参考ECharts官方文档,
没有发布过npm包的同学,可能会对NPM对开发有一种蜜汁敬畏,觉得这是一个很高大上的东西。甚至有次面试,面试官问我有没有发过npm包,当时只用过还没写过,我想应该挺难的,就小声说了没有,然后就让我回去了o(╯□╰)o。
其实,在现在的我看来,npm包就是一个我们平时经常写的一个export
出来的模块而已,只不过跟其它业务代码耦合性低,具有较高的独立性。
当然,要发布一个npm包,除了写的模块组件外,还需要做一些基础的包装工作。下面我就以最近开发的「DigitalKeyboard 数字键盘 NPM」 为例,一一列出具体步骤:
1、2、3足可以完成一个npm,4、5、6是为了开发一个高质量的npm。
具体代码移步github,请反手 给个 ★ Star ^_~。完整目录结构如下:
├── LICENSE
├── README.md
├── build
│ └── Keyboard.js
├── config
│ └── webpack
│ ├── webpack.base.config.js
│ ├── webpack.config.js
│ ├── webpack.dev.config.js
│ └── webpack.prod.config.js
├── index.html
├── package.json
├── src
│ ├── Keyboard.js
│ ├── Keyboard.scss
│ └── main.js
├── test
│ └── Keyboard.test.js
└── yarn.lock
现在只需要看src目录下的三个文件。其中,main.js 主要是对将要开发模块的引用,只需存在于开发阶段,同时作为此阶段webpack的入口文件,核心代码在Keyboard.js。
这里,主要用的是ES6的class
和export default
,Keyboard的核心**就是点击哪个键就对外输出什么内容,实现也比较简单,大家都能看得懂,这里就不展开讲了,具体可以看github 源码。
这一步也不用说,大家直接去官网注册就好了。
{
"name": "digital-keyboard",
"version": "1.0.0",
"main": "build/Keyboard.js",
"repository": "https://github.com/simbawus/DigitalKeyboard.git",
"author": "simbawu <[email protected]>",
"description": "DigitalKeyboard Component",
"keywords": [
"DigitalKeyboard",
"Digital",
"Keyboard",
]
}
此时的配置文件也比较简单,只需配置npm包名,准备用的名字现在npm搜索一下,已经存在的就不能用了;版本号version,每次发布版本号都需要更新,不然发布不成功;对外export
的文件路径,这里我用的是webpack打包后的文件,如果不用webpack,直接引用src/Keyboard.js也可以,只不过要做模块化方式兼容,这个后面说。也可以放上项目所在github地址及作者名,description和keywords比较利于SEO,不过这些都不是必需项。
到这里,一个npm包就开发完成了,直接发布即可使用。但是,略显粗糙:代码压缩、单元测试、readme都没写,别人不知道怎么用也不敢用。下面一步步完善。
这里用的是最新版的webpack4,官方提供production和development两种开发模式,并分别做了默认压缩处理,非常适合这里。有两点要特别说明下:
libraryTarget: 'umd'
umd
有的同学可能不是太熟悉,但是cmd、amd大家应该都知道,分别应用于服务端和浏览器端的模块方案。umd就是前面提到的模块化方式兼容。感兴趣可以参考我的另一篇文章JavaScript Module 设计解析及总结。
production和development的entry不一样:
development的entry是main.js,而production的entry是Keyboard.js。前面说过,开发阶段需要有对模块的引用,但是正式发布就不需要了,所以要分别配置。
其他就不展开讲了,我的webpack配置结构很清晰,欢迎大家直接copy。
├── webpack.base.config.js
├── webpack.config.js
├── webpack.dev.config.js
└── webpack.prod.config.js
大家经常看到很多不错的项目都有,这就像一个证明可用性的证书,给人安全感和信任感,所以添加单元测试,还是很有必要的,同时也可以提高代码质量。先介绍需要用到的几个概念:
mocha:测试框架;
chai:断言库,断言通俗来讲就是判断代码结果对不对;
jsdom:node端是没有js dom对象的,比如window、document等等,所以需要这个库提供;
istanbul:代码覆盖率计算工具;
coveralls:统计上面的代码测试覆盖率工具;
travis-ci:自动集成,比如master代码push到github上之后,travis-ci就会自动进行自动化测试。
这里介绍下jsdom的用法,当时按照几个文档来都跑不通:
const {JSDOM} = require('jsdom');
const {window} = new JSDOM(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0, maximum-scale=1.0,user-scalable=no">
<meta name="author" content="吴胜斌,simbawu">
<title>数字键盘</title>
</head>
<body>
<div id="values"></div>
<div id="app"></div>
</body>
</html>`);
propagateToGlobal(window);
function propagateToGlobal(window) {
for (let key in window) {
if (!window.hasOwnProperty(key)) continue;
if (key in global) continue;
global[key] = window[key];
}
}
首先引入jsdom,然后构造一个document,并引入其中的window对象然后一一赋值给node的global对象。其实也很简单,只不过第一次接触,而且找的文档写的也不清楚,所以花了点时间。其他几个文档都还不错,可以看看文档再看看我是怎么用的。此时的package.json就很很丰富了,可以执行yarn test
和yarn cover
看看测试是否通过及测试覆盖率。
一个好的readme是决定用户用不用你项目的关键因素,所以要多花点心思,千万不能忽略。
#先登录NPM账号:
npm login
#会依次让你输入用户名、密码、和邮箱
Username: simbawu
Password:
Email: (this IS public) [email protected]
#登录成功会出现以下提示信息:
Logged in as simbawu on https://registry.npmjs.org/.
#执行发布命令:
npm publish
#发布成功后会出现以下提示信息:
+ [email protected]
#这里digital-keyboard是我的NPM包名,1.0.0是包的版本号
接下来,我们可以在npm官网,通过搜索包名或者在个人中心看到刚刚发布的包。
本文首发于github,欢迎Follow、Watch 和 Star 。◕‿◕。
作为一个程序员,搭建一个个人博客几乎是所有人的需求,一来比较酷,二来也可以记录自己的学习和生活总结。但如果你不是全栈工程师,实现这个需求还是有点麻烦。后端搭建一套现有的前端框架及前端写API都还是有一定门槛的,当然,如果你是大牛当我没说,哈哈哈!
下面,我将介绍一个特别简单的方法,甚至不用写代码,执行几个命令就可以搭建一个博客,就算你不是程序员,也是So easy。那就是:fork我的博客。为什么说fork我的博客就可以搭建一个博客呢?博客重要的是有内容,并且可以随时更新,而不是一个静态页。这就要用到本文的核心:GitHub GraphQL API,这是github提供的一个开放式的API。我们只需要将文章用Markdown写好后,放到博客项目Issues里面,然后通过这个api,获取我们的写的文章,再前端渲染,就可以啦!!是不是特别棒,都不要写API,也不用考虑文章存哪。下面我来介绍如何实现:
请求GitHub GraphQL API,首先需要按照以下步骤在github申请一个access token:
右上角个人头像 > Settings > Developer settings > Personal access tokens > Generate new token
然后在Token description 写好关于这个token的描述,在Select scopes选择相应的权限,这里只需要user > read:user 就可以,点击Generate token按钮后会跳转到token列表页,这时需要马上把这个token记录下来,因为这是敏感数据,刷新后就没有了,不然得重新申请。
建议大家直接Fork我的项目 simbawus/blog,再修改相应配置,这样可以免去开发的成本,并且这个项目会持续更新,配置修改及启动可查看我项目的README。当然也可以fork后进行二次开发。也十分鼓励大家从零开始开发,也顺便练练手。
关于GraphQL的介绍,可查看我些的这篇文章前端应该知道的GraphQL。
GitHub GraphQL API的文档并没有使用示例,如果之前没用过GraphQL API,还是有点懵的,下面我举三个常见的例子说明下,具体可以看我博客代码,别忘了Star噢~。
通常,我们会在博客首页设计一个有分类的文章列表,这就要求在发布Issue时需要选择对应的label。先看官方label文档:
Connections 里面有issues,所以在查询labels的同时,还可以查询issues。先列出要传输的数据data,核心也在这:
data = {
query: `query {
repository(owner:"simbawus", name: "blog") {
issues(orderBy:{field: UPDATED_AT, direction: DESC} , labels: null, first: 10, after: ${page}) {
edges{
cursor
node{
title
updatedAt
bodyText
number
}
}
}
labels(first: 100){
nodes{
name
}
}
}
}`
};
repository
代表查询指定的仓库,括号里的参数owner代表这个仓库的所有者,name代表仓库名称。issues
表示要查询的issue列表,里面的参数表示这个列表的条件:orderBy
为排序方式,根据更新时间UPDATED_AT和倒序DESC来,labels
为null,说明查询的是所有issues,first
表示一次查询返回的issues数量,after
传上一个issue的id,可用来分页,最终这次请求拿到的数据结构如下,完整的请浏览器查看:
{
"data": {
"repository": {
"issues": {
"edges": {
"0": {
"cursor": "Y3Vyc29yOnYyOpK5MjAxOC0wNC0yNlQxMDoyNjoxNiswODowMM4S8hYL",
"node": {
"bodyText": "作为一个程序员...",
"number": "11",
"title": "如何利用GitHub GraphQL API开发个人博客?",
"updatedAt": "2018-04-22T03:46:34Z",
}
}
}
},
"labels": {
"nodes": {
"0": {
"name": "JavaScript"
}
}
}
}
}
}
search这个connections的文档写的让我一脸懵逼,摸索了好久才写出来,大家可以试着按官网文档写一下。
let data = {
query: `query {
search(query:"${keyWords} repo:simbawus/blog", type: ISSUE, first: 10) {
issueCount
edges{
cursor
node{
... on Issue {
title
number
bodyText
updatedAt
}
}
}
}
}`
};
search的query参数类型为String!,表示一个非空的字符串,怎么也想不到要这么写才行吧?query:"${keyWords} repo:simbawus/blog"
。node 这个fields的文档,看的也是二脸懵逼,还好想到es6的扩展符。
最重要的是文章内容这部分了,传输数据比较简单:
let data = {
query: `query {
repository(owner:"simbawus", name: "blog") {
issue(number: ${articleId}) {
title
updatedAt
bodyHTML
}
}
}`
};
请求直接返回一段HTML,问题是如何处理这段HTML,格式化并且高亮文章里面的代码。这里我用的是React的dangerouslySetInnerHTML
和github的css库github-markdown-css
,核心代码如下:
import 'github-markdown-css';
class ArticlePage extends React.Component {
_renderHTML() {
return { __html: this.state.bodyHTML };
}
render() {
return (
<div className = 'markdown-body'
dangerouslySetInnerHTML = { this._renderHTML() } >
</div>
);
}
}
结合GitHub GraphQL API开发个人博客的核心内容基本就这么多了,具体代码欢迎查看github:simbawus/blog,一起踩坑。
文章同步于以下社区,可以选一个关注我噢 。◕‿◕。
由于微信公众号、微博等平台不可控因素,文章持续被删,故转载,希望尽己之力,能让更多人看到:github。
2001年,东北一家国有疫苗公司悄无声息进行改制。多年后再回首,人们才明白其中意义。
那年的9月18日,上市公司长春高新旗下的长生生物迎来了两位新的股东——韩刚君和杜伟民。
韩刚君用1932万元买下了长生生物30%的股权,成为第二大股东;他和杜伟民的合资公司则成为了长生的小股东。
杜伟民是长生生物的销售总监。
这笔交易几乎没人注意到。长生生物被放到聚光灯下,是在两年后了。
2003年末,长春高新和长生生物的掌门人高俊芳把2000万打进公司账户,要将长生生物私有化。
算下来,高俊芳的出价是每股2.4元;而当时多位竞标者表示,他们愿意出3元/股的价格。
高俊芳很感谢他们的出价,然后拒绝了他们。
这引起了漫天质疑,有人把低价贱卖国有资产的举报信寄到了市政府。但仍然没有阻挡高俊芳成为长生生物第三大股东。
终于,高俊芳、韩刚君和杜伟民走到一起,他们手中握着长生生物的大半股份。
2007年,韩刚君把自己的股份卖给了高俊芳,帮助后者成为长生生物的绝对控制人。
十年后再回首,他们手中已经掌握了**疫苗的半壁江山——最大的乙肝疫苗企业、最大的流感疫苗企业、第二大水痘疫苗企业、第二和第四大狂犬病疫苗企业……
他们生产的疫苗,每天都源源不断,注入你和你孩子的身体中。
就在高俊芳顶着资本市场的唾骂,完成长生生物私有化的时候,韩刚君与杜伟民已经南下,他们盯上了刚刚拿到狂犬病疫苗生产资质的常州延申生物。
很快,韩刚君与杜伟民以2000万元拿下了常州延申90%的股份,将其改组成为江苏延申,韩刚君担任董事长。
杜伟民在加拿大远程完成了这一切。他这时已经拿到了加拿大绿卡,只要再待几年,他就能成为**人民老朋友白求恩大夫的老乡。
之后不到三年的时间里,韩刚君为江苏延申拿到了流感疫苗、气管炎疫苗、疖病疫苗的生产批文。江苏延申很快成为**最大的流感疫苗供应商和第四大狂犬病疫苗公司。
2007年10月,韩刚君和杜伟民已经准备好了上市资料。如果不是一次偶然的发现,江苏延申将会登陆资本市场。
2009年3月,大连金港迪安狂犬疫苗在抽检中被发现造假,食药监总局马上对狂犬疫苗生产企业进行突击检查,江苏延申被查出五批产品涉嫌造假。
食药监局发现,延申偷工减料、弄虚作假、逃避监管,疫苗抗原含量低于国家标准,达不到药效。
北大医学部的专家将注射失效的疫苗总结为两个字——杀人。
但这时,江苏延申的18万份疫苗已经流入21个省107个疾控中心,全部被注射进了病人体中。
江苏延申表示,我们无能为力。
没有召回、没有补偿。案件发生后,江苏延申因为生产、销售伪劣产品罪,被判处罚金三百万元,总经理和五名员工被判刑。
董事长韩刚君和另一位大股东却毫发无伤。
更诡异的是,江苏延申很快东山再起,仅仅半年之后,就获得了防疫部门160万人份甲流订单,价格超过亿元;不久又获得了甲流疫苗生产牌照。
就在调查组的眼皮子底下,杜伟民把这个生产假疫苗企业的股份全部转让出去,套现两亿元,顺利退出延申。
杜伟民的眼前是一片星辰大海。
根据一篇人物报道,2007年,杜伟民毅然变卖了加拿大的家产,放弃了入籍,带着妻儿回国了,重新投身疫苗领域。
“我熟悉这个行业,清楚**的生物产业是要发展的。而且我在国外越来越觉得,让疫苗技术掌握在**人自己的手上,关系到国家的生物安全”。
**真是一片化腐朽为神奇的土地。在海外迷失了自己,回国就对了。
不过,杜先生没有放弃加拿大绿卡和香港身份证。
2008年开始,杜伟民悄无声息地在产权交易所吃下了深圳老国企康泰生物的大部分股份,控制了这家**最大的乙肝疫苗生产商。
深圳康泰的乙肝疫苗技术,来自于美国人的人道主义援助。1989年9月,美国默克公司以微不足道的700万美元向**转让了全套工艺技术。
杜伟民把当年重组江苏延申的手法用在了康泰生物的重组上,开始为上市铺路。
疫苗企业上市,最大的障碍是产品种类单一,康泰多年来上市无功而返,就是因为只有乙肝疫苗这一种产品。
很快,杜先生通过收购北京民海生物,让康泰的产品多样化。2012年底到2013年,康泰自主研发的三款疫苗——Hib疫苗、麻风疫苗、四联疫苗获准生产,先后上市。
民海生物是怎么在短时间内获得三款产品的生产许可?
北京高级法院的一则审判书显示,2010年到2014年间,国家食品药品监督管理局药品审议中心副主任尹红章收受杜伟民47万元,为民海生物的药品申报审批事宜提供帮助。
除了加快新疫苗上市,杜伟民也让康泰的主打产品乙肝疫苗获得了新生。
当年杜伟民还在长生生物做销售时,**的疫苗市场基本被国有七大生物制品公司瓜分,互不侵犯。杜伟民瞅准时机,用拉低价格的方式抢占山头。
他在康泰复制了这一手段,在政府的招标中,康泰用低价摆平了一切竞争者。
乙肝疫苗单人份的出厂价格本来在15元左右,康泰直接报价6.9元,迅速拿下了国家免疫规划疫苗项目的大笔订单。对于竞标的研究所来说,这比它们的成本价还低,毕竟国家规定的出厂价都需要9.3元。
2013年12月,康泰和杜伟民迎来了最大危机。在十天时间里,共有8名新生儿在接种康泰的乙肝疫苗后死亡。
一个月后,食药总局和卫计委的调查显示,所有的婴儿死亡为偶合性死亡,疫苗质量没有问题,向康泰生物归还了生产证书。 风波过后,康泰安然上市,市值从杜伟民收购时的6亿元飙升到现在的400亿元。
高俊芳、韩刚君与杜伟民三位疫苗之王,有太多共同点。
他们对疫苗企业的控制和改造路径相似。瞄准的都是老牌疫苗企业,长生生物、延申生物和深圳康泰,背后都是**国有的生物制剂研究所。
他们以非常低廉的价格迅速入手,实现完全控股,然后在短时间内拿到多个疫苗生产牌照,为将来上市铺路。
最重要的一点,这些企业所属的有关部门似乎完全没有意识到疫苗生产牌照的价值——疫苗的毛利普遍在80%以上。
高俊芳买下长生生物时,企业估值为1.2亿元,2015年借壳上市时,市值为55亿元;
2008年杜伟民吃进深圳康泰时,企业估值为6亿元,2016年上市首日,市值达到138亿元;
江苏延申没能上市,不过韩刚君和杜伟民是以2222万元的估值入股的,不过就在他们重组的那三年,延申的净利润总额就达到8400万元。
兽爷的好友你包叔说:
都是九年义务教育,科研工作者怎么这么好骗。
康泰生物上市前的半年内,公司的股权在个人和机构之间进行了20多次倒手。
深交所曾经质疑其中是否存在利益输送和国资流失,要求公司说明转让的原因及合理性等,但康泰始终没有详细回答这个问题。
高俊芳则用同样的手法,把自己的儿子、老公、小姑子、外甥和侄女全部变成了长生生物的股东。
就像360安全卫士,安装后,你会发现有各种360软件出现在你的电脑里。
2017胡润百富榜中,杜伟民以73亿元的身价位列第559位,高俊芳家族以51亿位列第820位。
如果不是江苏延申的狂犬疫苗出了问题,韩刚君也该出现在这个名单上的。
疫苗之王们都起于草莽,没有人知道他们的钱从哪来的。
高俊芳入股长生生物的4000多万全部是自筹资金,当时上市公司的公告显示,她的月薪只有6000元。高俊芳说自己掏了200万,其余的钱一会说是亲友凑的,一会说是和银行贷款。
杜伟民和韩刚君之前分别是江西省卫生防疫站和河南开封龙亭区卫生防疫站的普通员工。下海不过四五年时间,成了疫苗行业最重要的资本推手。
都是天才。生子当如防疫员。
世界上最大的军火出口国是美俄英法中,联合国安理会的五个常任理事国。
**新闻事件最多的,也是这些疫苗之王们。
2018年7月11日,长生生物内部的一名员工实名举报疫苗生产存在造假。国家药监局马上对长生生物进行飞行检查,发现狂犬病疫苗生产存在记录造假。
国家药监局已要求吉林省局收回长春长生的药品GMP证书,长春长生主动召回有效期内所有批次的狂犬病疫苗。
在对长生生物调查的时候,吉林食药监管局“顺手”对其两年前的违规行为进行了处罚。
2017年11月,食品药品监管总局接到报告,在抽检中发现长生生物一个批次的百白破疫苗效价指标不符合标准规定,接种后可能会影响接种儿童的白喉、破伤风和百日咳的免疫效果。
这时25万支疫苗已经全部销往山东,打入25万多名儿童的身体。
8个月过去,吉林有关部门行动迅速,没收了库存的186支疫苗,对长生生物罚款300多万。
186支,长生生物的库存真多啊。300万,处罚力度真大呀。
于是很快有了第二次造假。
短短三年时间,长生生物狂犬病疫苗的市场占有率,就从不到4%上升到28%,成为**第二大狂犬病疫苗供应商,正在威胁行业霸主成大生物的市场地位。
成大生物疫苗的报价是149元,长春长生的报价则是239元,而且还要比成大生物多打一针。
生物制药行业的朋友说:
活了这么久,竟然见到价格更高、针次更多的产品,把价格更低、针次更少的行业老大给压下去。
兽爷发现,长生生物2017年销售费用为5.83亿元,也就是说25个销售人员每人的销售费用是2330万元,是康泰生物的4倍,是成大生物的47倍。
兽爷就是租了个摊位卖煎饼的。这些数字是什么意思,我是一点都看不懂的。对了,城管来了,我要去搞好下关系去了。
1989年,在默克公司总裁罗伊·瓦杰洛斯的主导下,乙肝疫苗生产技术被以极低的价格送给**人民。他说,预防医学是最好的医学,对付传染性疾病的最好方法是预防它。
那时,这位乙肝英雄应该没有想到,传染病可以预防,有些事却无法预防。
穷病真的是没法治的?
(来自微信公号:兽楼处(ID: ishoulc);作者:兽爷)
由于计算机二进制环境下浮点数的计算精度缺失,导致我们用JS进行数据计算的时候会出现很多意想不到的情况:
console.log(70-67.9); //2.0999999999999943
我们预期的结果是2.1,结果却来了这么一大串…,为了达到预期的结果,我们可以采用以下解决方案:
console.log((70-67.9).toFixed(1)); //2.1
toFixed看似完美的解决了我们的问题,其实,坑才刚刚挖好:
整数也有小数点
console.log((70.9-67.9).toFixed(1)); //3.0
虽然是保留一位小数,但是3.0这种显示结果并不优雅,不过问题不大,简单处理一下即可:保留几(n)位小数我们就乘10n。
console.log((70.9-67.9).toFixed(1)*10/10); //3
属于银行家舍入法
银行家舍入:所谓银行家舍入法,其实质是一种四舍六入五取偶(又称四舍六入五留双)法。简单来说就是:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。
我们先看现状
console.log(0.005.toFixed(2)); // 0.01 正确
console.log(0.015.toFixed(2)); // 0.01 错误
console.log(0.025.toFixed(2)); // 0.03 正确
console.log(0.035.toFixed(2)); // 0.04 正确
console.log(0.045.toFixed(2)); // 0.04 错误
console.log(0.055.toFixed(2)); // 0.06 正确
console.log(0.065.toFixed(2)); // 0.07 正确
console.log(0.075.toFixed(2)); // 0.07 错误
console.log(0.085.toFixed(2)); // 0.09 正确
console.log(0.095.toFixed(2)); // 0.10 正确
虽然并不完全符合银行家舍入法的规则(不符合的原因应该是二进制下浮点数的坑导致的),但toFixed所存在的问题已经找到了。
重写toFixed方法解决以上两个问题:
Number.prototype.toFixed = function(length)
{
var carry = 0; //存放进位标志
var num,multiple; //num为原浮点数放大multiple倍后的数,multiple为10的length次方
var str = this + ''; //将调用该方法的数字转为字符串
var dot = str.indexOf("."); //找到小数点的位置
if(str.substr(dot+length+1,1)>=5) carry=1; //找到要进行舍入的数的位置,手动判断是否大于等于5,满足条件进位标志置为1
multiple = Math.pow(10,length); //设置浮点数要扩大的倍数
num = Math.floor(this * multiple) + carry; //去掉舍入位后的所有数,然后加上我们的手动进位数
var result = num/multiple + ''; //将进位后的整数再缩小为原浮点数
return result;
}
下面我们来验证结果:
console.log(0.015.toFixed(2)); // 0.02 正确
console.log((70.9-67.9).toFixed(1)); //3 正确
调用重写方法,可以一劳永逸的解决toFixed存在的问题。当然,我们也有更直接的方法来处理,见方法2。
Math.round() 四舍五入取整
比如,0.015(num)需要保留两(n)位小数,可先将这个数乘以100(10*n),四舍五入取整后,再除以100(10*n)。
console.log(Math.round(0.015*100)/100); //0.02
Math.round(num*(10*n))/(10*n); //通用公式,n为要保留的小数位数
此方法简单快速,在数据处理比较少的地方很实用。
扫码看岗位,简历建议直接发邮箱 [email protected],标题格式:简历-姓名-岗位
推荐码:PCSXW5Y0W8
🤠 一上来就放JD太过正式,画饼又没诚意,先说说我们实打实的福利吧:
你心动了吗?🌝
小红书是年轻人的生活方式平台,创立于2013。小红书以“Inspire Lives 分享和发现世界的精彩”为使命,用户可以通过短视频、图文等形式记录生活点滴,分享生活方式,并基于兴趣形成互动。截至到2019年10月,小红书月活跃用户数已经过亿,其中70%用户是90后,并持续快速增长。
小红书的业务主要由三大块构成:
具体岗位JD我就不贴了,扫描内推码进去就可以看到要求。
列几个老板们很看中的点吧:
扫码看岗位,简历建议直接发邮箱 [email protected],标题格式:简历-姓名-岗位
推荐码:PCSXW5Y0W8
需要了解内推进度可加我微信,暂时不看机会的也可以加,未来可能会用到。
上海、北京、武汉都有Office:
期待你的加入! 👏 👏 👏
周一入职,同事JJ让我熟悉一下基于React的新项目。
按照以往,我的步骤都是:
git clone xxx
npm install
npm run dev
这时,JJ给我来了下面一段
git clone xxx
yarn
yarn start
“咦,yarn是什么鬼?难道npm更高级的替代品?为什么要替代npm?难道有什么好的地方?”,内心一连串的问题冒出来。我就默默的问了一下JJ:“yarn是跟npm一样的东西吗?”,“嗯。”JJ忙碌的敲着键盘,显然这个问题不值得继续问下去了。我也默默的把刚才脑子里一连串的问题记了下来。
“Yarn是由Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具 ,正如官方文档中写的,Yarn 是为了弥补 npm 的一些缺陷而出现的。”这句话让我想起了使用npm时的坑了:
npm install
的时候巨慢。特别是新的项目拉下来要等半天,删除node_modules,重新install的时候依旧如此。"5.0.3",
"~5.0.3",
"^5.0.3"
“5.0.3”表示安装指定的5.0.3版本,“~5.0.3”表示安装5.0.X中最新的版本,“^5.0.3”表示安装5.X.X中最新的版本。这就麻烦了,常常会出现同一个项目,有的同事是OK的,有的同事会由于安装的版本不一致出现bug。
带着这些坑,我开始了解Yarn的优势及其解决的问题。
npm | yarn |
---|---|
npm install | yarn |
npm install react --save | yarn add react |
npm uninstall react --save | yarn remove react |
npm install react --save-dev | yarn add react --dev |
npm update --save | yarn upgrade |
有了yarn的压力之后,npm做了一些类似的改进。
在npm5.0之前,yarn的优势特别明显。但是在npm之后,通过以上一系列对比,我们可以看到 npm5 在速度和使用上确实有了很大提升,值得尝试,不过还没有超过yarn。
综上我个人的建议是如果你已经在个人项目上使用 yarn,并且没有遇到更多问题,目前完全可以继续使用。但如果有兼容 npm 的场景,或者身处在使用 npm,cnpm,tnpm 的团队,以及还没有切到 yarn 的项目,那现在就可以试一试 npm5 了。
文章同步于以下社区,可以选一个关注我噢 。◕‿◕。
⁽⁽ଘ( ˊᵕˋ )ଓ⁾⁾ ◡̈⃝ ♡ .^◡^. ᵔ.ᵔ ᵔ◡ᵔ
ʚɞ ❆ ✿ ❁ 𓃰 ⍢⃝
⠒̫⃝ ʚ◡̈⃝ɞ '◡' ✺ •́.•̀ •͈˽•͈
⁎ ๑ ❀ ✧ ˃̣̣̥᷄⌓˂̣̣̥᷅ ⚇
˙Ꙫ˙ ଲ ̊ଳ ̊ ⸝⸝⸝⸝◟̆◞̆♡ ₍₍ ง⍢⃝ว ⁾⁾
⍢⃝ ⍤⃝ ⍥⃝ ⍨⃝ ⍩⃝⃜ ˙Ꙫ˙
◟̊◞̊ ◟́◞̀ ◟̆◞̆ ◡̈ ᵔ.ᵔ ᵔ◡ᵔ '◡' ´͈ ᵕ `͈ .^◡^.
₍₍ ง⍢⃝ว ⁾⁾ ꈍ .̮ ꈍ (•̶̑ ૄ •̶̑) ◉‿◉
⚗︎·̫⚗︎ (⌃·̫⌃) ( ⁼̴̤̆◡̶͂⁼̴̤̆ ) ( ๑•◡ુ-๑) (ͼ̤͂ ͜ ͽ̤͂)✧
⚆_⚆ (˘❥˘) (•̀⌓•́)シ (人 •͈ᴗ•͈)۶♡♡
ಥ_ಥ ʚ⃛ɞ ᖗ( ᐛ )ᖘ ❛‿˂̵✧ ੧ᐛ੭
̑̑ᗦ↞◃ 𐂂 ̑̑ෆ⃛ Ծ‸Ծ ( ´͈ ᵕ `͈ )◞♡
ʕง•ᴥ•ʔง ʕ•ᴥ•ʔ ʕ ᵔᴥᵔ ʔ
ʕ•̀ ω •́ʔ ʕ•̀ o •́ʔ (•̀ᴗ•́)و ̑̑
ฅʕ•̫͡•ʔฅ (ง ˙o˙)ว ´͈ ᵕ `͈
ฅ⁽͑ ˚̀ ˙̭ ˚́ ⁾̉ฅ ⋆ᶿ̵᷄ ˒̼ ᶿ̵᷅⋆ ヽ( ຶ▮ ຶ)ノ!!!
´•ﻌ•` •﹏• ˃̣̣̥᷄⌓˂̣̣̥᷅ ( ⁼̴̀ .̫ ⁼̴́ )✧
(•̀⌓•́)シ (๑ ꒪̇ꌂ̇꒪̇๑) (๑・▱・๑)
(•̶̑ ૄ •̶̑) ˘̩̩̩ε˘̩ƪ | ᐕ)⁾⁾ ୧⍢⃝୨
٩( ᐛ )و ಠ‿ಠ ღ( ´・ᴗ・` )
ꉂ೭(˵¯̴͒ꇴ¯̴͒˵)౨” ꉂ(ˊᗜˋ*) (⁎⁍̴̛ᴗ⁍̴̛⁎)
ʕ•̫͡•ོʔ•̫͡•ཻʕ•̫͡•ʔ•͓͡•ʔ ᕙ(•̤᷆ ॒ ູ॒•̤᷇)ᕘ
o(´^`)o ٩(´◒`)۶ ٩(˃̶͈̀௰˂̶͈́)و
( ง⁼̴̀ω⁼̴́)ง⁼³₌₃ ( ˃᷄˶˶̫˶˂᷅ ) (ง ˙o˙)ว
(๑⁼̴̀д⁼̴́๑) (•ૢ⚈͒⌄⚈͒•ૢ) ₍₍ (̨̡ ‾᷄ᗣ‾᷅ )̧̢ ₎₎
✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧ ᶘ ᵒᴥᵒᶅ (´-㉨ก`)
罒㉨罒 乀(ˉεˉ乀) (◍˃̶ᗜ˂̶◍)✩
(˶‾᷄ꈊ‾᷅˵) ( ・᷄ὢ・᷅ ) ⊂(˃̶͈̀ε ˂̶͈́ ⊂ )
(;´༎ຶД༎ຶ`) (๑•̀ㅂ•́)و✧ ˚‧º·(˚ ˃̣̣̥᷄⌓˂̣̣̥᷅ )‧º·˚
٩(๑ᵒ̴̶̷͈᷄ᗨᵒ̴̶̷͈᷅)و (৹ᵒ̴̶̷᷄́ฅᵒ̴̶̷᷅৹) (⸝⸝⸝ᵒ̴̶̷̥́ ⌑ ᵒ̴̶̷̣̥̀⸝⸝⸝) (*꒦ິ⌓꒦ີ)
(⑉・̆-・̆⑉) (๑⃙⃘´༥๑⃙⃘)。 (ง ´͈౪
͈)ว
٩(•̤̀ᵕ•̤́๑)ᵒᵏᵎᵎᵎᵎ ( ͡° ͜ʖ ͡°)✧ (๑>ڡ<)☆
✧( •˓◞•̀ ) ଘ(੭ˊᵕˋ)੭* ੈ✩‧₊˚
⁽⁽ଘ( ˊᵕˋ )ଓ⁾⁾ ( *・ω・)✄ (=̴̶̷̤̄ ₃ =̴̶̷̤̄)♡
(-᷅_-᷄) Ծ‸Ծ ٩(•̤̀ᵕ•̤́๑)ᵒᵏᵎᵎᵎᵎ ❤️๑•́ ₃ •̀๑❤
( ・᷄д・᷅ ) (ᵒ̤̑ ₀̑ ᵒ̤̑) ಥ_ಥ ˃̣̣̥᷄⌓˂̣̣̥᷅ ˚‧º·(˚ ˃̣̣̥᷄⌓˂̣̣̥᷅ )‧º·˚
ฅ՞•ﻌ•՞ฅ (๑•́ωก̀๑) (ฅ́˘ฅ̀)♡ (´-㉨ก`)
ଲଇଉକ 𐂂 𐂃 𐂄 𐂅
ˁ῁̭ˀˁ῁̮ˀˁ῁̱ˀˁ῁̥ˀˁ῁̼ˀˁ῁̩ˀˁ῁̬ˀ ʕ•̫͡•ོʔ•̫͡•ཻʕ•̫͡•ʔ•͓͡•ʔ
☀︎ ☁︎ ♥ ✿ ❁ ❆ ʚɞ ♡ ☃︎ 𓃰
JS 模块化规范目前主要分为:CommonJS、AMD、CMD、UMD 和ES6。
ES6为名门正派,其它则为开发者草拟的野生规范
//定义模块 math.js
function add(a, b){
return a + b
}
module.exports = {
add: add
}
//加载模块
var math = require('math');
math.add(2, 3);
因为require是同步的,math方法只能在math.js加载完之后才能运行,在浏览器环境如果网络不够快,会报错,所以CommonJS只适用于服务器端,也催生了AMD("Asynchronous Module Definition")的诞生。
由于模块化不是javascript原生支持,使用AMD需要对应的库RequireJS。
define(id, dependencies, factory);
id:可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)
dependencies:是一个当前模块依赖的模块名称数组
factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值 。
//定义模块 math.js
define(['dependency'], function(dependency){
function add(a, b){
return a + b
}
return {
add: add
};
});
//加载模块
require(['math'], function (math) {
math.add(2, 3);
});
AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块:dependency。
CMD跟AMD类似,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同 。
//定义模块 math.js
define(function(requie, exports, module){
var dependency = require('dependency');
function add(a, b){
return a + b
}
return {
add: add
};
});
//加载模块
seajs.use(['math'], function (math) {
math.add(2, 3);
});
CMD推崇就近依赖,只有在用到某个模块('dependency')的时候再去require。
AMD模块以浏览器第一的原则发展,异步加载模块,CommonJS模块以服务器第一原则发展,选择同步加载,UMD是AMD和CommonJS的糅合。
既然要通用,怎么办呢?分别判断是否支持AMD和CommonJS,这就是所谓的UMD。
((root, factory) => {
if (typeof define === 'function' && define.amd) {
//AMD
define(['dependency'], factory);
} else if (typeof exports === 'object') {
//CommonJS
var dependency = requie('dependency');
module.exports = factory(dependency);
} else {
//都不是,浏览器全局定义
root.returnExports = factory(root.dependency);
}
})(this, (dependency) => {
//do something... 这里是真正的函数体
function add(a, b){
return a + b
}
return {
add: add
};
});
require/exports 的用法只有以下三种简单的写法:
const fs = require('fs')
exports.fs = fs
module.exports = fs
而 import/export 的写法就多种多样:
import fs from 'fs'
import {default as fs} from 'fs'
import * as fs from 'fs'
import {readFile} from 'fs'
import {readFile as read} from 'fs'
import fs, {readFile} from 'fs'
export default fs
export const fs
export function readFile
export {readFile, read}
export * from 'fs'
正则表达式regex
是用于匹配字符串中字符组合的模式,由参数pattern
+ 标志flags
构成。
指所有字母、数字、符号等
指换行、回车、空白等不会实际显示出来的字符
字符 | 说明 |
---|---|
\cx | 匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。 |
\f | 匹配一个换页符。等价于 \x0c 和 \cL。 |
\n | 匹配一个换行符。等价于 \x0a 和 \cJ。 |
\r | 匹配一个回车符。等价于 \x0d 和 \cM。 |
\s | 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。 |
\S | 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。 |
\t | 匹配一个制表符。等价于 \x09 和 \cI。 |
\v | 匹配一个垂直制表符。等价于 \x0b 和 \cK。 |
指定匹配前面的子表达式必须要出现多少次才能满足
字符 | 说明 |
---|---|
* | 零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。* 等价于{0,}。 |
+ | 一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。 |
? | 零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 。? 等价于 {0,1}。 |
{n} | n >= 0。匹配确定的 n 次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 o。 |
{n,} | n >= 0。至少匹配 n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。 |
{n,m} | m >= n>= 0。例如,"o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'。逗号和两个数之间不能有空格。 |
描述字符串定边界
字符 | 说明 |
---|---|
^ | 匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 '\n' 或 '\r' 之后的位置。 |
$ | 匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 '\n' 或 '\r' 之前的位置。 |
\b | 匹配一个单词边界,也就是指单词和空格间的位置。例如, 'er\b' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。 |
\B | 匹配非单词边界。'er\B' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。 |
组,应用于限制多选结构的范围/分组/捕获文本/环视/特殊模式处理
字符 | 说明 |
---|---|
(abc) | 匹配 'abc' 并且记住匹配项,括号被称为捕获括号。模式/(foo) (bar) \1 \2/中的 '(foo)' 和 '(bar)' 匹配并记住字符串 "foo bar foo bar" 中前两个单词。模式中的 \1 和 \2 匹配字符串的后两个单词。注意 \1、\2、\n 是用在正则表达式的匹配环节。在正则表达式的替换环节,则要使用像 $1、$2、$n 这样的语法,例如,'bar foo'.replace( /(...) (...)/, '$2 $1' )。 |
(?:abc) | 匹配 'abc' 这样一组,但不记录,不保存到$变量中,否则可以通过$x取第几个括号所匹配到的项,比如:(aaa)(bbb)(ccc)(?:ddd)(eee),可以用$1获取(aaa)匹配到的内容,而$3则获取到了(ccc)匹配到的内容,而$4则获取的是由(eee)匹配到的内容,因为前一对括号没有保存变量 |
a(?=bbb) | 正向肯定查找,表示a后面必须紧跟3个连续的b。例如,"Windows(?=95|98|NT|2000)"能匹配"Windows2000"中的"Windows",但不能匹配"Windows3.1"中的"Windows"。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。 |
a(?!bbb) | 正向否定查找,表示a后面不能跟3个连续的b。例如"Windows(?!95|98|NT|2000)"能匹配"Windows3.1"中的"Windows",但不能匹配"Windows2000"中的"Windows"。 |
a(?<=bbb) | 反向肯定查找,表示a前面必须紧跟3个连续的b。例如,"(?<=95|98|NT|2000)Windows"能匹配"2000Windows"中的"Windows",但不能匹配"3.1Windows"中的"Windows"。 |
a(?<!bbb) | 反向否定查找,表示a前面不能跟3个连续的b。例如"(?<!95|98|NT|2000)Windows"能匹配"3.1Windows"中的"Windows",但不能匹配"2000Windows"中的"Windows"。 |
单个匹配,字符集/排除字符集/命名字符集
字符 | 说明 |
---|---|
[xyz] | 字符集合。匹配所包含的任意一个字符。例如, '[abc]' 可以匹配 "plain" 中的 'a'。 |
[^xyz] | 负值字符集合。匹配未包含的任意字符。例如, '[^abc]' 可以匹配 "plain" 中的'p'、'l'、'i'、'n'。 |
[a-z] | 字符范围。匹配指定范围内的任意字符。例如,'[a-z]' 可以匹配 'a' 到 'z' 范围内的任意小写字母字符。 |
[^a-z] | 负值字符范围。匹配任何不在指定范围内的任意字符。例如,'[^a-z]' 可以匹配任何不在 'a' 到 'z' 范围内的任意字符。 |
字符 | 说明 |
---|---|
\ | 将下一个字符标记为一个特殊字符、或一个原义字符、或一个 向后引用、或一个八进制转义符。例如,'n' 匹配字符 "n"。'\n' 匹配一个换行符,而 "\(" 则匹配 "("。 |
? | 当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 "oooo",'o+?' 将匹配单个 "o",而 'o+' 将匹配所有 'o'。 |
. | 匹配除换行符(\n、\r)之外的任何单个字符。要匹配包括 '\n' 在内的任何字符,请使用像"(.|\n)"的模式。 |
x|y | 匹配 x 或 y。例如,'z|food' 能匹配 "z" 或 "food"。'(z|f)ood' 则匹配 "zood" 或 "food"。 |
\d | 匹配一个数字字符。等价于 [0-9]。 |
\D | 匹配一个非数字字符。等价于 [^0-9]。 |
\w | 匹配字母、数字、下划线。等价于'[A-Za-z0-9_]'。 |
\W | 匹配非字母、数字、下划线。等价于 '[^A-Za-z0-9_]'。 |
\xn | 匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,'\x41' 匹配 "A"。'\x041' 则等价于 '\x04' & "1"。正则表达式中可以使用 ASCII 编码。 |
\num | 匹配 num,其中 num 是一个正整数。对所获取的匹配的引用。例如,'(.)\1' 匹配两个连续的相同字符 |
\n | 标识一个八进制转义值或一个向后引用。如果 \n 之前至少 n 个获取的子表达式,则 n 为向后引用。否则,如果 n 为八进制数字 (0-7),则 n 为一个八进制转义值。 |
\nm | 标识一个八进制转义值或一个向后引用。如果 \nm 之前至少有 nm 个获得子表达式,则 nm 为向后引用。如果 \nm 之前至少有 n 个获取,则 n 为一个后跟文字 m 的向后引用。如果前面的条件都不满足,若 n 和 m 均为八进制数字 (0-7),则 \nm 将匹配八进制转义值 nm。 |
\nml | 如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。 |
\un | 匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如, \u00A9 匹配版权符号 (?)。 |
有6个标志,可单独或一起使用
flags | 说明 |
---|---|
g | 全局搜索 |
i | 不区分大小写搜索 |
m | 多行搜索 |
u | 正确处理四个字节的 UTF-16 编码 |
y | 粘连搜索 |
s | dotAll模式,即点(dot)代表一切字符 |
有以下两种方式构建一个正则表达式:
pattern/flags
const regex = /ab+c/;
const regex = /hello/gi;
new RegExp(pattern [, flags])
let regex = new RegExp("ab+c");
let regex = new RegExp(/hello/, "gi");
let regex = new RegExp(/hello/gi);
let regex = new RegExp("hello", "gi");
var regex = /nn/;
let str = 'runnobnnnbnn';
方法 | 说明 | 示例 | 返回值 |
---|---|---|---|
test | 测试是否匹配,返回true或false | regex.test(str) | true |
exec | 查找匹配的内容,有则返回一个数组,未匹配则返回null | regex.exec(str) | ["nn", index: 2, input: "runnobnnnbnn", groups: undefined] |
match | 查找匹配的内容,有则返回一个数组,未匹配则返回null | str.match(regex) | ["nn", index: 2, input: "runnobnnnbnn", groups: undefined] |
search | 查找匹配的内容,有则返回位置索引,未匹配则返回-1 | str.match(regex) | 2 |
replace | 查找匹配的内容,并且使用替换字符str1串替换掉匹配到的子字符串 | str.replace(regex, str1) | ruccobnnnbnn |
split | 使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中 | str.split(regex) | ["ru", "ob", "nb", ""] |
regex.exec(str) // ["nn", index: 6, input: "runnobnnnbnn", groups: undefined]
str.match(regex) // ["nn", "nn", "nn"]
随着移动互联网的普及和快速发展,手机成了互联网行业最大的流量分发入口。以及随着5G的快速发展,未来越来越多的“端”也会如雨后春笋般快速兴起。而“快”作为互联网的生存之道,为了占领市场,企业也会积极跟进,快速布局。同一个应用,各个“端”独立开发,不仅开发周期长,而且人员成本高。同时,作为技术人员,也不应该满足于这种重复、低能的工作状态。在这样的形势下,跨平台的技术方案也受到越来越多人和企业的关注。接下来,我将从原理、优缺点等方面为大家分享《跨平台技术演进》。
说到跨平台,没人不知道H5。不管是在Mac、Windows、Linux、iOS、Android还是其他平台,只要给一个浏览器,连“月球”上它都能跑。
下面,我们来看看让H5如此横行霸道的浏览器的架构:
浏览器由以上7个部分组成,而“渲染引擎”是性能优化的重中之重,一起了解其中的渲染原理。
不同的浏览器内核不同,渲染过程会不太一样,但主要流程还是一致的。
分为下面6步骤:
从以上6步,我们可以总结渲染优化的要点:
以上就是浏览器端的内容。但H5作为跨平台技术的载体,是如何与不同平台的App进行交互的呢?这时候JSBridge就该出场了。
JSBridge,顾名思义,是JS和Native之间的桥梁,用来进行JS和Native之间的通信。
通信分为以下两个维度:
JavaScript 调用 Native,有两种方式:
(boohee://goods/876898)
,当web前端发送URL Scheme请求之后,Native 拦截到请求并根据URL Scheme进行相关操作。Native 调用 JavaScript:
JavaScript暴露一个对象如JSBridge给window,让Native能直接访问。
那么App内加载H5的过程是什么样的呢?
打开H5分为4个阶段:
这四步,对应的过程如上图所以,我们可以针对性的做性能优化。
下面,我们进行H5的优缺点分析:
优点
缺点
虽然H5目前还存在不足,但随着PWA、WebAssembly等技术的进步,相信H5在未来能够得到越来也好的发展。
2018年是微信小程序飞速发展的一年,19年,各大厂商快速跟进,已经有了很大的影响力。下面,我们以微信小程序为例,分析小程序的技术架构。
小程序跟H5一样,也是基于Webview实现。但它包含View视图层、App Service逻辑层两部分,分别独立运行在各自的WebView线程中。
可以理解为h5的页面,提供UI渲染。由WAWebview.js来提供底层的功能,具体如下:
每个窗口都有一个独立的WebView进程,因此微信限制不能打开超过5个层级的页面来保障用户体验。
提供逻辑处理、数据请求、接口调用。由WAService.js来提供底层的功能,具体如下:
运行环境:
仅有一个WebView进程
视图层和逻辑层通过系统层的JSBridage进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层将触发的事件通知到逻辑层进行业务处理。
优点
缺点
既然WebView性能不佳,那有没有更好的方案呢?下面我们看看React Native。
RN的理念是在不同平台上编写基于React的代码,实现Learn once, write anywhere。
Virtual DOM在内存中,可以通过不同的渲染引擎生成不同平台下的UI,JS和Native之间通过Bridge通信
在 React 框架中,JSX 源码通过 React 框架最终渲染到了浏览器的真实 DOM 中,而在 React Native 框架中,JSX 源码通过 React Native 框架编译后,与Native原生的UI组件进行映射,用原生代替DOM元素来渲染,在UI渲染上非常接近Native App。
优点
缺点
虽然RN还存在不足,但RN新版本已经做了如下改进,并且RN团队也在积极准备大版本重构,能否成为开发者们所信赖的跨平台方案,让我们拭目以待。
既然React Native在渲染方面还摆脱不了原生,那有没有一种方案是直接操控GPU,自制引擎渲染呢,我们终于迎来了Flutter!
Flutter是Google开发的一套全新的跨平台、开源UI框架,支持iOS、Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件。渲染引擎依靠跨平台的Skia图形库来实现,依赖系统的只有图形绘制相关的接口,可以在最大程度上保证不同平台、不同设备的体验一致性,逻辑处理使用支持AOT的Dart语言,执行效率也比JavaScript高得多。
很多人会好奇,为什么Flutter要用Dart,而不是用JavaScript开发,这里列下Dart的优势
优点
缺点
移动互联网的普及和快速发展,跨平台技术风起云涌,这也是技术发展过程中的必经之路,等浪潮退去,才知道谁在裸泳。我个人更看好H5或类H5方案,给它一个浏览器,连“月球”都能跑,这才是真正的跨平台,其他都是浮云。
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.