Giter VIP home page Giter VIP logo

blog's Introduction

Hi! I'm Simba. Welcome to my Github Profile 👋

I'm a Frontend Developer in Shanghai/China

blog's People

Contributors

wusb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

blog's Issues

小红书内推 ~ 快来!

长期内推

扫码看岗位,简历建议直接发邮箱 [email protected],标题格式:简历-姓名-岗位

logo

推荐码:PCSXW5Y0W8

令人心动的福利

🤠 一上来就放JD太过正式,画饼又没诚意,先说说我们实打实的福利吧:

  • 营养搭配的免费3餐,品类多多随心选
  • 咖啡、果汁、红绿茶、肥宅快乐水等随心饮
  • 零食、冷饮随心吃
  • 免费健身房,熟悉我的人都知道我雷打不动每周至少三次的健身习惯,吃胖了可以一起去运动
  • 上下班不打卡,再也不用担心迟到扣钱了
  • 法定带薪年假+5个工作日福利年假+5个工作日全薪病假,有这么多带薪假的公司不多了
  • 商业保险,虽然不希望用上,但关键是时刻能帮大忙
  • 白纸黑字15.5薪,再也不用辛辛苦苦干一年,年终奖还看老板心情了
  • 公司1.5公里范围内1000元租房补贴,每天走路上下班再也不用挤地铁了
  • 最新款MacBookPro + 4K显示器,不用担心卡、慢、看不清等本不应该担心的问题
  • 节日礼品、生日贺礼、年度体检等等,人文关怀超级棒
  • ......,还有很多,有时间我再迭代

你心动了吗?🌝

我们要做的事情

小红书是年轻人的生活方式平台,创立于2013。小红书以“Inspire Lives 分享和发现世界的精彩”为使命,用户可以通过短视频、图文等形式记录生活点滴,分享生活方式,并基于兴趣形成互动。截至到2019年10月,小红书月活跃用户数已经过亿,其中70%用户是90后,并持续快速增长。

小红书的业务主要由三大块构成:

  1. 社区:小红书社区目前内容覆盖时尚、个护、彩妆、美食、旅行、娱乐、读书、健身、母婴等各个生活方式领域,每天产生超过70亿次的笔记曝光,其中超过95%为UGC内容
  2. 企业号:小红书企业号部门围绕“企业号”这一核心产品,整合公司从社区营销一直到交易闭环的资源,更好地连接消费者和品牌,帮助品牌在小红书完成一站式闭环营销,提供全链条服务
  3. 福利社:小红书福利社是小红书的自营电商平台,在小红书福利社,用户可以一键购买来自全世界的优质美妆、时尚、家电、零食商品

我们期待的队友

具体岗位JD我就不贴了,扫描内推码进去就可以看到要求。

列几个老板们很看中的点吧:

  • 向上
  • 好奇心
  • 团队合作精神

快来加入我们吧

扫码看岗位,简历建议直接发邮箱 [email protected],标题格式:简历-姓名-岗位

logo

推荐码:PCSXW5Y0W8

需要了解内推进度可加我微信,暂时不看机会的也可以加,未来可能会用到。

logo

上海、北京、武汉都有Office:

  • 上海总部:上海市 黄浦区 马当路388号 SOHO复兴广场C座
  • 北京:北京市 朝阳区 安定路 中海国际B座18楼
  • 武汉:武汉市 光谷大道 现代光谷世贸中心B座10楼

期待你的加入! 👏 👏 👏

从0开始发布一个NPM包

(这篇文章以React为例,其他框架类同。)

预备工作

如何发布一个NPM包

  1. 首先初始化一个React项目,也可以直接Fork我之前用React初始化的一个Demo。这个Demo之前也写过一篇文章介绍,目的就是让大家真正理解每一步的由来。从0开始利用Webpack搭建React Demo
  2. NPM官网,注册个人账号
  3. 给自己的NPM包取一个名字,并且通过搜索你取的这个名字的方式在NPM官网验证这个名字是否存在,如果存在需要重新换一个独一无二的名字。确定名字之后,将package.json文件的name 换成你确定的名字,例如我作为案例的组件名:react-components-calendar

开始开发

  1. 根据我Demo里面的目录结构,建议先在components目录开发我们的组件,然后在page目录进行引用。

  2. 配置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的基础上,主要做了三项改进:

    1. 入口entry项新增了component这一项目,用来作为我们开发的组件的入口。

    2. 输出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时的模块名。

    3. 扩展externals,这项配置不处理的某些以依赖库。比如,我们的组件是基于React的,如果我们的组件也是用在React项目里,那组件依赖的React就不需要进行babel转义了。如果转义,那样打出来的包就会特别大,没必要。不过在测试开发阶段,这项应该注释掉,在打包发布之前加上就可以。

  3. 配置package.json

    "main": "build/component.js"

    只需要在main这项定义打包后的组件的位置就OK了。

打包发布

  1. 执行yarn build打包后,在build目录会生成app.js文件和component.js文件。但是我们这里只需要component.js文件,因为我没有对webpack.config.js做环境的区分,所以会将entry项的文件都打包出来。现在我们webpack配置比较简单,所以需要手动处理,将app.js删除后再提交。

  2. 现在开始发布

    #先登录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是包的版本号
  3. 接下来,我们可以在NPM官网,通过搜索包名或者在个人中心看到刚刚发布的包。

    NPM

测试验证

  1. 安装刚刚发布的包

    yarn add react-components-calendar --dev
  2. 安装成功后,在之前我们引入开发的组件的地方将引用路径替换为包引用

    // import Calendar from '../../../components/index.js'
    import Calendar from 'react-components-calendar'
  3. 注释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'
      // },
  4. 启动项目,查看效果。

    yarn start

    Calendar Preview

最后,祝贺大功告成!赶紧去发自己的第一个包吧~

附上本项目的源码:react-components-calendar

前端应该知道的GraphQL

作为一个程序员,搭建一个个人博客几乎是所有人的需求,一来比较酷,二来也可以记录自己的学习和生活总结。但如果你不是全栈工程师,实现这个需求还是有点麻烦。后端搭建一套现有的前端框架及前端写API都还是有一定门槛的,当然,如果你是大牛当我没说,哈哈哈!

下面,我将介绍一个特别简单的方法,甚至不用写代码,执行几个命令就可以搭建一个博客,就算你不是程序员,也是So easy。那就是:fork我的博客。为什么说fork我的博客就可以搭建一个博客呢?博客重要的是有内容,并且可以随时更新,而不是一个静态页。这就要用到本文的核心:GitHub GraphQL API,这是github提供的一个开放式的API。我们只需要将文章用Markdown写好后,放到博客项目Issues里面,然后通过这个api,获取我们的写的文章,再前端渲染,就可以啦!!是不是特别棒,都不要写API,也不用考虑文章存哪。下面我来介绍如何实现:

获取access token

请求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 API数据

关于GraphQL的介绍,可查看我些的这篇文章前端应该知道的GraphQL

GitHub GraphQL API的文档并没有使用示例,如果之前没用过GraphQL API,还是有点懵的,下面我举三个常见的例子说明下,具体可以看我博客代码,别忘了Star噢~。

获取标签及相关issues

通常,我们会在博客首页设计一个有分类的文章列表,这就要求在发布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,一起踩坑。

欢迎讨论,点个赞再走吧~

文章同步于以下社区,可以选一个关注我噢 。◕‿◕。

simbawu | github | segmentfault | 知乎 | 简书 | 掘金

微信小程序 wx.getBackgroundAudioManager API中的一些坑

(此总结只保证对基础库1.9.91有效)

onCanplay

onCanplay在真机及开发工具上均无法触发,因此无法在此生命周期执行相关操作

onPlay

onPlay开始播放音频,我们通常会考虑在这个生命周期内获取音频的长度duration,但是回调函数内并不能实时获取,感觉违背了常识。可以采用setTimeout设定一个延时来获取,但也做不到实时,建议在 onTimeUpdate回调内获取我们需要的duration。

onPause

在此生命周期,我们希望能获取音频的播放状态paused,来做一些逻辑上的处理,但并不能准备获取正确的值true,此处需要手动设置。比如:

const audio = wx.getBackgroundAudioManager();
audio.onPause(() => {
    //此时audio.paused,按道理来说是true,并不一定是true
    self.setData({
        pause: audio.paused
    })
    //所以需要显式的去设置
    self.setData({
        pause: true
    })
})

seek

文档写的参数是postion,如果是首次接触很容易误导,其实参数就是一个Number,比如:

const audio = wx.getBackgroundAudioManager();
audio.seek(500);

title

如果不设置的话,在iOS设备上会导致音频无法播放,Android上影响不大。

全局对象

const audio = wx.getBackgroundAudioManager();

audio 获取是一个全局的音频对象,如果一个页面里面定义多个audio,它们的生命周期是共享的。比如说,我们在同一个page的两个function内均定义了audio,并且,都使用了onTimeUpdate这个生命周期,那么这两个onTimeUpdate是共享的,并不会同时存在,只会以最近一次调用的audio为准。

正则学习手册

定义

正则表达式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)代表一切字符

创建

有以下两种方式构建一个正则表达式:

  1. 字面量:由包含在斜杠之间的模式组成
  • 格式: pattern/flags
const regex = /ab+c/;

const regex = /hello/gi;
  • 优点:加载时编译,性能好
  1. 构造函数
  • 格式: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", ""]
  • match:非全局匹配时,跟exec很相似;全局匹配时,就大不同了,上面的例子,在全局匹配时的返回值如下:
regex.exec(str) // ["nn", index: 6, input: "runnobnnnbnn", groups: undefined]

str.match(regex) // ["nn", "nn", "nn"]

参考文档

超可爱的颜文字,我要放到代码里❛‿˂̵✧

⁽⁽ଘ( ˊᵕˋ )ଓ⁾⁾ ◡̈⃝ ♡ .^◡^. ᵔ.ᵔ ᵔ◡ᵔ

ʚɞ ❆ ✿ ❁ 𓃰 ⍢⃝

⠒̫⃝ ʚ◡̈⃝ɞ '◡' ✺ •́.•̀ •͈˽•͈

⁎ ๑ ❀ ✧ ˃̣̣̥᷄⌓˂̣̣̥᷅ ⚇

˙Ꙫ˙ ଲ ̊ଳ ̊ ⸝⸝⸝⸝◟̆◞̆♡ ₍₍ ง⍢⃝ว ⁾⁾

⍢⃝ ⍤⃝ ⍥⃝ ⍨⃝ ⍩⃝⃜ ˙Ꙫ˙

◟̊◞̊ ◟́◞̀ ◟̆◞̆ ◡̈ ᵔ.ᵔ ᵔ◡ᵔ '◡' ´͈ ᵕ `͈ .^◡^.

₍₍ ง⍢⃝ว ⁾⁾ ꈍ .̮ ꈍ (•̶̑ ૄ •̶̑) ◉‿◉

⚗︎·̫⚗︎ (⌃·̫⌃) ( ⁼̴̤̆◡̶͂⁼̴̤̆ ) ( ๑•◡ુ-๑) (ͼ̤͂ ͜ ͽ̤͂)✧

⚆_⚆ (˘❥˘) (•̀⌓•́)シ (人 •͈ᴗ•͈)۶♡♡

ಥ_ಥ ʚ⃛ɞ ᖗ( ᐛ )ᖘ ❛‿˂̵✧ ੧ᐛ੭

̑̑ᗦ↞◃ 𐂂 ̑̑ෆ⃛ Ծ‸Ծ ( ´͈ ᵕ `͈ )◞♡

ʕง•ᴥ•ʔง ʕ•ᴥ•ʔ ʕ ᵔᴥᵔ ʔ

ʕ•̀ ω •́ʔ ʕ•̀ o •́ʔ (•̀ᴗ•́)و ̑̑

ฅʕ•̫͡•ʔฅ (ง ˙o˙)ว ´͈ ᵕ `͈

ฅ⁽͑ ˚̀ ˙̭ ˚́ ⁾̉ฅ ⋆ᶿ̵᷄ ˒̼ ᶿ̵᷅⋆ ヽ( ຶ▮ ຶ)ノ!!!

´•ﻌ•` •﹏• ˃̣̣̥᷄⌓˂̣̣̥᷅ ( ⁼̴̀ .̫ ⁼̴́ )✧

(•̀⌓•́)シ (๑ ꒪̇ꌂ̇꒪̇๑) (๑・▱・๑)

(•̶̑ ૄ •̶̑) ˘̩̩̩ε˘̩ƪ | ᐕ)⁾⁾ ୧⍢⃝୨

٩( ᐛ )و ಠ‿ಠ ღ( ´・ᴗ・` )

ꉂ೭(˵¯̴͒ꇴ¯̴͒˵)౨” ꉂ(ˊᗜˋ*) (⁎⁍̴̛ᴗ⁍̴̛⁎)

ʕ•̫͡•ོʔ•̫͡•ཻʕ•̫͡•ʔ•͓͡•ʔ ᕙ(•̤᷆ ॒ ູ॒•̤᷇)ᕘ

o(´^`)o ٩(´◒`)۶ ٩(˃̶͈̀௰˂̶͈́)و

( ง⁼̴̀ω⁼̴́)ง⁼³₌₃ ( ˃᷄˶˶̫˶˂᷅ ) (ง ˙o˙)ว

(๑⁼̴̀д⁼̴́๑) (•ૢ⚈͒⌄⚈͒•ૢ) ₍₍ (̨̡ ‾᷄ᗣ‾᷅ )̧̢ ₎₎

✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧ ᶘ ᵒᴥᵒᶅ (´-㉨ก`)

罒㉨罒 乀(ˉεˉ乀) (◍˃̶ᗜ˂̶◍)✩

(˶‾᷄ꈊ‾᷅˵) ( ・᷄ὢ・᷅ ) ⊂(˃̶͈̀ε ˂̶͈́ ⊂ )

(;´༎ຶД༎ຶ`) (๑•̀ㅂ•́)و✧ ˚‧º·(˚ ˃̣̣̥᷄⌓˂̣̣̥᷅ )‧º·˚

٩(๑ᵒ̴̶̷͈᷄ᗨᵒ̴̶̷͈᷅)و (৹ᵒ̴̶̷᷄́ฅᵒ̴̶̷᷅৹) (⸝⸝⸝ᵒ̴̶̷̥́ ⌑ ᵒ̴̶̷̣̥̀⸝⸝⸝) (*꒦ິ⌓꒦ີ)

(⑉・̆-・̆⑉) (๑⃙⃘´༥๑⃙⃘)。 (ง ´͈౪͈)ว

٩(•̤̀ᵕ•̤́๑)ᵒᵏᵎᵎᵎᵎ ( ͡° ͜ʖ ͡°)✧ (๑>ڡ<)☆

✧( •˓◞•̀ ) ଘ(੭ˊᵕˋ)੭* ੈ✩‧₊˚

⁽⁽ଘ( ˊᵕˋ )ଓ⁾⁾ ( *・ω・)✄ (=̴̶̷̤̄ ₃ =̴̶̷̤̄)♡

(-᷅_-᷄) Ծ‸Ծ ٩(•̤̀ᵕ•̤́๑)ᵒᵏᵎᵎᵎᵎ ❤️๑•́ ₃ •̀๑❤

( ・᷄д・᷅ ) (ᵒ̤̑ ₀̑ ᵒ̤̑) ಥ_ಥ ˃̣̣̥᷄⌓˂̣̣̥᷅ ˚‧º·(˚ ˃̣̣̥᷄⌓˂̣̣̥᷅ )‧º·˚

ฅ՞•ﻌ•՞ฅ (๑•́ωก̀๑) (ฅ́˘ฅ̀)♡ (´-㉨ก`)

ଲଇଉକ 𐂂 𐂃 𐂄 𐂅

ˁ῁̭ˀˁ῁̮ˀˁ῁̱ˀˁ῁̥ˀˁ῁̼ˀˁ῁̩ˀˁ῁̬ˀ ʕ•̫͡•ོʔ•̫͡•ཻʕ•̫͡•ʔ•͓͡•ʔ

☀︎ ☁︎ ♥ ✿ ❁ ❆ ʚɞ ♡ ☃︎ 𓃰

JS 处理数据中的一些坑

由于计算机二进制环境下浮点数的计算精度缺失,导致我们用JS进行数据计算的时候会出现很多意想不到的情况:

console.log(70-67.9);   //2.0999999999999943

我们预期的结果是2.1,结果却来了这么一大串…,为了达到预期的结果,我们可以采用以下解决方案:

  1. toFixed(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。

  1. 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为要保留的小数位数

此方法简单快速,在数据处理比较少的地方很实用。

如何利用GitHub GraphQL API开发个人博客?

作为一个程序员,搭建一个个人博客几乎是所有人的需求,一来比较酷,二来也可以记录自己的学习和生活总结。但如果你不是全栈工程师,实现这个需求还是有点麻烦。后端搭建一套现有的前端框架及前端写API都还是有一定门槛的,当然,如果你是大牛当我没说,哈哈哈!

下面,我将介绍一个特别简单的方法,甚至不用写代码,执行几个命令就可以搭建一个博客,就算你不是程序员,也是So easy。那就是:fork我的博客。为什么说fork我的博客就可以搭建一个博客呢?博客重要的是有内容,并且可以随时更新,而不是一个静态页。这就要用到本文的核心:GitHub GraphQL API,这是github提供的一个开放式的API。我们只需要将文章用Markdown写好后,放到博客项目Issues里面,然后通过这个api,获取我们的写的文章,再前端渲染,就可以啦!!是不是特别棒,都不要写API,也不用考虑文章存哪。下面我来介绍如何实现:

获取access token

请求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 API数据

关于GraphQL的介绍,可查看我些的这篇文章前端应该知道的GraphQL

GitHub GraphQL API的文档并没有使用示例,如果之前没用过GraphQL API,还是有点懵的,下面我举三个常见的例子说明下,具体可以看我博客代码,别忘了Star噢~。

获取标签及相关issues

通常,我们会在博客首页设计一个有分类的文章列表,这就要求在发布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,一起踩坑。

欢迎讨论,点个赞再走吧~

文章同步于以下社区,可以选一个关注我噢 。◕‿◕。

simbawu | github | segmentfault | 知乎 | 简书 | 掘金

npm和yarn的区别,我们该如何选择?

周一入职,同事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是什么?

“Yarn是由Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具 ,正如官方文档中写的,Yarn 是为了弥补 npm 的一些缺陷而出现的。”这句话让我想起了使用npm时的坑了:

  • npm install 的时候巨慢。特别是新的项目拉下来要等半天,删除node_modules,重新install的时候依旧如此。
  • 同一个项目,安装的时候无法保持一致性。由于package.json文件中版本号的特点,下面三个版本号在安装的时候代表不同的含义。
"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。

  • 安装的时候,包会在同一时间下载和安装,中途某个时候,一个包抛出了一个错误,但是npm会继续下载和安装包。因为npm会把所有的日志输出到终端,有关错误包的错误信息就会在一大堆npm打印的警告中丢失掉,并且你甚至永远不会注意到实际发生的错误

带着这些坑,我开始了解Yarn的优势及其解决的问题。

Yarn的优点?

  • 速度快 。速度快主要来自以下两个方面:
  1. 并行安装:无论 npm 还是 Yarn 在执行包的安装时,都会执行一系列任务。npm 是按照队列执行每个 package,也就是说必须要等到当前 package 安装完成之后,才能继续后面的安装。而 Yarn 是同步执行所有任务,提高了性能。
  2. 离线模式:如果之前已经安装过一个软件包,用Yarn再次安装时之间从缓存中获取,就不用像npm那样再从网络下载了。
  • 安装版本统一:为了防止拉取到不同的版本,Yarn 有一个锁定文件 (lock file) 记录了被确切安装上的模块的版本号。每次只要新增了一个模块,Yarn 就会创建(或更新)yarn.lock 这个文件。这么做就保证了,每一次拉取同一个项目依赖时,使用的都是一样的模块版本。npm 其实也有办法实现处处使用相同版本的 packages,但需要开发者执行 npm shrinkwrap 命令。这个命令将会生成一个锁定文件,在执行 npm install 的时候,该锁定文件会先被读取,和 Yarn 读取 yarn.lock 文件一个道理。npm 和 Yarn 两者的不同之处在于,Yarn 默认会生成这样的锁定文件,而 npm 要通过 shrinkwrap 命令生成 npm-shrinkwrap.json 文件,只有当这个文件存在的时候,packages 版本信息才会被记录和更新。
  • 更简洁的输出:npm 的输出信息比较冗长。在执行 npm install 的时候,命令行里会不断地打印出所有被安装上的依赖。相比之下,Yarn 简洁太多:默认情况下,结合了 emoji直观且直接地打印出必要的信息,也提供了一些命令供开发者查询额外的安装信息。
  • **多注册来源处理:**所有的依赖包,不管他被不同的库间接关联引用多少次,安装这个包时,只会从一个注册来源去装,要么是 npm 要么是 bower, 防止出现混乱不一致。
  • 更好的语义化: yarn改变了一些npm命令的名称,比如 yarn add/remove,感觉上比 npm 原本的 install/uninstall 要更清晰。

Yarn和npm命令对比

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

npm的未来:npm5.0

有了yarn的压力之后,npm做了一些类似的改进。

  1. 默认新增了类似yarn.lock的 package-lock.json;
  2. git 依赖支持优化:这个特性在需要安装大量内部项目(例如在没有自建源的内网开发),或需要使用某些依赖的未发布版本时很有用。在这之前可能需要使用指定 commit_id 的方式来控制版本。
  3. 文件依赖优化:在之前的版本,如果将本地目录作为依赖来安装,将会把文件目录作为副本拷贝到 node_modules 中。而在 npm5 中,将改为使用创建 symlinks 的方式来实现(使用本地 tarball 包除外),而不再执行文件拷贝。这将会提升安装速度。目前yarn还不支持。

总结

在npm5.0之前,yarn的优势特别明显。但是在npm之后,通过以上一系列对比,我们可以看到 npm5 在速度和使用上确实有了很大提升,值得尝试,不过还没有超过yarn。

综上我个人的建议是如果你已经在个人项目上使用 yarn,并且没有遇到更多问题,目前完全可以继续使用。但如果有兼容 npm 的场景,或者身处在使用 npm,cnpm,tnpm 的团队,以及还没有切到 yarn 的项目,那现在就可以试一试 npm5 了。

欢迎讨论,点个赞再走吧~

文章同步于以下社区,可以选一个关注我噢 。◕‿◕。

simbawu | github | segmentfault | 知乎 | 简书 | 掘金

ECharts in React

ECharts是众所周知的一个百度出品的数据可视化图形框架,前两周在跟阿里健康合作的项目体重曲线正好有用到,借助ECharts的定制化开发,可高度还原产品及设计要求。

Calendar Preview

跟直接引用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官方文档

低门槛彻底理解JavaScript中的深拷贝和浅拷贝

在说深拷贝与浅拷贝前,我们先看两个简单的案例:

//案例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自有方法是否支持:

Array

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()只能实现一维数组的深拷贝

具备同等特性的还有:concatArray.from()

Object

  1. Object.assign()
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()也只能实现一维对象的深拷贝

  1. JSON.parse(JSON.stringify(obj))
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噢~。

欢迎讨论,点个赞再走吧~

文章同步于以下社区,可以选一个关注我噢 。◕‿◕。

simbawu | github | segmentfault | 知乎 | 简书 | 掘金

跨平台技术演进

前言

大家好,我是simbawu,关于这篇文章,有问题欢迎来这里讨论。

随着移动互联网的普及和快速发展,手机成了互联网行业最大的流量分发入口。以及随着5G的快速发展,未来越来越多的“端”也会如雨后春笋般快速兴起。而“快”作为互联网的生存之道,为了占领市场,企业也会积极跟进,快速布局。同一个应用,各个“端”独立开发,不仅开发周期长,而且人员成本高。同时,作为技术人员,也不应该满足于这种重复、低能的工作状态。在这样的形势下,跨平台的技术方案也受到越来越多人和企业的关注。接下来,我将从原理、优缺点等方面为大家分享《跨平台技术演进》。

H5

说到跨平台,没人不知道H5。不管是在Mac、Windows、Linux、iOS、Android还是其他平台,只要给一个浏览器,连“月球”上它都能跑。

浏览器架构

下面,我们来看看让H5如此横行霸道的浏览器的架构:

浏览器架构

  • User Interface 用户界面:提供用户与浏览器交互
  • Browser Engine 浏览器引擎:控制渲染引擎与JS解释器
  • Rendering Engine 渲染引擎:负责页面渲染
  • JavaScript Interpreter JS解释器:执行JS代码,输出结果给渲染引擎
  • Networking 网络工作组:处理网络请求
  • UI Backend UI后端:绘制窗口小部件
  • Data Storage 数据存储:管理用户数据

浏览器由以上7个部分组成,而“渲染引擎”是性能优化的重中之重,一起了解其中的渲染原理。

渲染引擎原理

不同的浏览器内核不同,渲染过程会不太一样,但主要流程还是一致的。

WebKit 主流程

分为下面6步骤:

  1. HTML解析出DOM Tree
  2. CSS解析出CSSOM
  3. DOM Tree与CSSOM关联生成Render Tree
  4. Layout 根据Render Tree计算每个节点的尺寸、位置
  5. Painting 根据计算好的信息绘制整个页面的像素信息
  6. Composite 将多个复合图层发送给GPU,GPU会将各层合成,然后显示在屏幕上。

从以上6步,我们可以总结渲染优化的要点:

  • Layout在浏览器渲染过程中比较耗时,应尽可能避免重排的产生
  • 复合图层占用内存比重非常高,可采用减小复合图层进行优化

以上就是浏览器端的内容。但H5作为跨平台技术的载体,是如何与不同平台的App进行交互的呢?这时候JSBridge就该出场了。

JSBridge原理

JSBridge,顾名思义,是JS和Native之间的桥梁,用来进行JS和Native之间的通信。

JS

通信分为以下两个维度:

  • JavaScript 调用 Native,有两种方式:

    1. 拦截URL Scheme:URL Scheme是一种类似于url的链接(boohee://goods/876898),当web前端发送URL Scheme请求之后,Native 拦截到请求并根据URL Scheme进行相关操作。
    2. 注入API:通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。
  • Native 调用 JavaScript
    JavaScript暴露一个对象如JSBridge给window,让Native能直接访问。

那么App内加载H5的过程是什么样的呢?

App打开H5过程

打开H5分为4个阶段:

  1. 交互无反馈
  2. 打开页面 白屏
  3. 请求API,处于loading状态
  4. 出现数据,正常展现

这四步,对应的过程如上图所以,我们可以针对性的做性能优化。

优缺点分析

下面,我们进行H5的优缺点分析:

优点

  • 跨平台:只要有浏览器,任何平台都可以访问
  • 开发成本低:生态成熟,学习成本低,调试方便
  • 迭代速度快:无需审核,及时响应,用户可毫无感知使用最新版

缺点

  • 性能问题:在反应速度、流畅度、动画方面远不及原生
  • 功能问题:对摄像头、陀螺仪、麦克风等硬件支持较差

虽然H5目前还存在不足,但随着PWA、WebAssembly等技术的进步,相信H5在未来能够得到越来也好的发展。

小程序

2018年是微信小程序飞速发展的一年,19年,各大厂商快速跟进,已经有了很大的影响力。下面,我们以微信小程序为例,分析小程序的技术架构。

小程序跟H5一样,也是基于Webview实现。但它包含View视图层、App Service逻辑层两部分,分别独立运行在各自的WebView线程中。

View

可以理解为h5的页面,提供UI渲染。由WAWebview.js来提供底层的功能,具体如下:

  • 消息通信封装为WeixinJSBridge
  • 日志组件Reporter封装
  • wx api(UI相关)
  • 小程序组件实现和注册
  • VirtualDOM,Diff和Render UI实现
  • 页面事件触发

每个窗口都有一个独立的WebView进程,因此微信限制不能打开超过5个层级的页面来保障用户体验。

App Service

提供逻辑处理、数据请求、接口调用。由WAService.js来提供底层的功能,具体如下:

  • 日志组件Reporter封装
  • wx api
  • App,Page,getApp,getCurrentPages等全局方法
  • AMD模块规范的实现

运行环境:

  • iOS:JavaScriptCore
  • Andriod:X5内核,基于Mobile Chrome 53/57
  • DevTool:nwjs Chrome 内核

仅有一个WebView进程

View & App Service通信

视图层和逻辑层通过系统层的JSBridage进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层将触发的事件通知到逻辑层进行业务处理。

优缺点分析

优点

  • 预加载WebView,准备新页面渲染
  • View层和逻辑层分离,通过数据驱动,不直接操作DOM
  • 使用Virtual DOM,进行局部更新
  • 组件化开发

缺点

  • 仍使用WebView渲染,并非原生渲染,体验不佳
  • 不能运行在非微信环境内
  • 没有window、document对象,不能使用基于浏览器的JS库
  • 不能灵活操作 DOM,无法实现较为复杂的效果
  • 页面大小、打开页面数量都受到限制

既然WebView性能不佳,那有没有更好的方案呢?下面我们看看React Native。

React Native

RN的理念是在不同平台上编写基于React的代码,实现Learn once, write anywhere。

Virtual DOM在内存中,可以通过不同的渲染引擎生成不同平台下的UI,JS和Native之间通过Bridge通信

React Native 工作原理

在 React 框架中,JSX 源码通过 React 框架最终渲染到了浏览器的真实 DOM 中,而在 React Native 框架中,JSX 源码通过 React Native 框架编译后,与Native原生的UI组件进行映射,用原生代替DOM元素来渲染,在UI渲染上非常接近Native App。

React Native 与Native平台通信

  • React Native用JavaScriptCore作为JS的解析引擎,在Android上,需要应用自己附带JavaScriptCore,iOS上JavaScriptCore属于系统的一部分,不需要应用附带。
  • 用Bridge将JS和原生Native Code连接起来。Native和 JavaScript 两端都保存了一份配置表,里面标记了所有Native暴露给 JavaScript 的模块和方法。交互通过传递 ModuleId、MethodId 和 Arguments 进行。

优缺点分析

优点

  • 垮平台开发:相比原生的ios 和 android app各自维护一套业务逻辑大同小异的代码,React Native 只需要同一套javascript 代码就可以运行于ios 和 android 两个平台,在开发、测试和维护的成本上要低很多。
  • 快速编译:相比Xcode中原生代码需要较长时间的编译,React Native 采用热加载的即时编译方式,使得App UI的开发体验得到改善,几乎做到了和网页开发一样随时更改,随时可见的效果。
  • 快速发布:React Native 可以通过 JSBundle 即时更新 App。相比原来冗长的审核和上传过程,发布和测试新功能的效率大幅提高。
  • 渲染和布局更高效:React Native摆脱了WebView的交互和性能问题,同时可以直接套用网页开发中的css布局机制。脱了 autolayout 和 frame 布局中繁琐的数学计算,更加直接简便。

缺点

  • 动画性能差:React Native 在动画效率和性能的支持还存在一些问题,性能上不如原生Api。
  • 不能完全屏蔽原生平台:就目前的React Native 官方文档中可以发现仍有部分组件和API都区分了Android 和 IOS 版本,即便是共享组件,也会有平台独享的函数。也就是说仍不能真正实现严格意义上的“一套代码,多平台使用”。另外,因为仍对ios 和android的原生细节有所依赖,所以需要开发者若不了解原生平台,可能会遇到一些坑。
  • 生态不完善:缺乏很多基本控件,第三方开源质量良莠不齐

展望未来

虽然RN还存在不足,但RN新版本已经做了如下改进,并且RN团队也在积极准备大版本重构,能否成为开发者们所信赖的跨平台方案,让我们拭目以待。

  1. 改变线程模式。UI 更新不再同时需要在三个不同的线程上触发执行,而是可以在任意线程上同步调用 JavaScript 进行优先更新,同时将低优先级工作推出主线程,以便保持对 UI 的响应。
  2. 引入异步渲染能力。允许多个渲染并简化异步数据处理。
  3. 简化 JSBridge,让它更快、更轻量。

既然React Native在渲染方面还摆脱不了原生,那有没有一种方案是直接操控GPU,自制引擎渲染呢,我们终于迎来了Flutter!

Flutter

Flutter是Google开发的一套全新的跨平台、开源UI框架,支持iOS、Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件。渲染引擎依靠跨平台的Skia图形库来实现,依赖系统的只有图形绘制相关的接口,可以在最大程度上保证不同平台、不同设备的体验一致性,逻辑处理使用支持AOT的Dart语言,执行效率也比JavaScript高得多。

Flutter架构原理

  • Framework:由Dart实现,包括Material Design风格的Widget,Cupertino(针对iOS)风格的Widgets,文本/图片/按钮等基础Widgets,渲染,动画,手势等。此部分的核心代码是:flutter仓库下的flutter package,以及sky_engine仓库下的io,async,ui(dart:ui库提供了Flutter框架和引擎之间的接口)等package。
  • Engine:由C++实现,主要包括:Skia,Dart和Text。
    • Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API。其已作为Google Chrome,Chrome OS,Android, Mozilla Firefox, Firefox OS等其他众多产品的图形引擎,支持平台还包括Windows7+,macOS 10.10.5+,iOS8+,Android4.1+,Ubuntu14.04+等。Skia作为渲染/GPU后端,在Android和Fuchsia上使用FreeType渲染,在iOS上使用CoreGraphics来渲染字体。
    • Dart部分主要包括:Dart Runtime,Garbage Collection(GC),如果是Debug模式的话,还包括JIT(Just In Time)支持。Release和Profile模式下,是AOT(Ahead Of Time)编译成了原生的arm代码,并不存在JIT部分。
    • Text即文本渲染,其渲染层次如下:衍生自minikin的libtxt库(用于字体选择,分隔行)。HartBuzz用于字形选择和成型。
  • Embedder:是一个嵌入层,即把Flutter嵌入到各个平台上去,这里做的主要工作包括渲染Surface设置,线程设置,以及插件等。从这里可以看出,Flutter的平台相关层很低,平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。

Dart优势

很多人会好奇,为什么Flutter要用Dart,而不是用JavaScript开发,这里列下Dart的优势

  • Dart 的性能更好。Dart在 JIT模式下,速度与 JavaScript基本持平。但是 Dart支持 AOT,当以 AOT模式运行时,JavaScript便远远追不上了。速度的提升对高帧率下的视图数据计算很有帮助。
  • Native Binding。在 Android上,v8的 Native Binding可以很好地实现,但是 iOS上的 JavaScriptCore不可以,所以如果使用 JavaScript,Flutter 基础框架的代码模式就很难统一了。而 Dart的 Native Binding可以很好地通过 Dart Lib实现。
  • Fuchsia OS。Fuchsia OS内置的应用浏览器就是使用 Dart语言作为 App的开发语言。

优缺点分析

优点

  • 性能强大:在两个平台上重写了各自的UIKit,对接到平台底层,减少UI层的多层转换,UI性能可以比肩原生
  • 优秀的语言特性:参考上面Dart优势分析
  • 路由设计优秀:Flutter的路由传值非常方便,push一个路由,会返回一个Future对象(也就是Promise对象),使用await或者.then就可以在目标路由pop,回到当前页面时收到返回值。

缺点

  • 优点即缺点,Dart 语言的生态小,精通成本比较高
  • UI控件API设计不佳
  • 与原生融合障碍很多,不利于渐进式升级

总结

移动互联网的普及和快速发展,跨平台技术风起云涌,这也是技术发展过程中的必经之路,等浪潮退去,才知道谁在裸泳。我个人更看好H5或类H5方案,给它一个浏览器,连“月球”都能跑,这才是真正的跨平台,其他都是浮云。

从0开始发布一个无依赖、高质量的npm

写在前面

没有发布过npm包的同学,可能会对NPM对开发有一种蜜汁敬畏,觉得这是一个很高大上的东西。甚至有次面试,面试官问我有没有发过npm包,当时只用过还没写过,我想应该挺难的,就小声说了没有,然后就让我回去了o(╯□╰)o。

其实,在现在的我看来,npm包就是一个我们平时经常写的一个export出来的模块而已,只不过跟其它业务代码耦合性低,具有较高的独立性。

当然,要发布一个npm包,除了写的模块组件外,还需要做一些基础的包装工作。下面我就以最近开发的「DigitalKeyboard 数字键盘 NPM」 为例,一一列出具体步骤:

  1. 写基础模块代码;
  2. 注册npm账号;
  3. 配置package.json;
  4. 配置webpack;
  5. 添加单元测试;
  6. 完善README.md;
  7. 发布

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的classexport default,Keyboard的核心**就是点击哪个键就对外输出什么内容,实现也比较简单,大家都能看得懂,这里就不展开讲了,具体可以看github 源码。

注册npm账号

这一步也不用说,大家直接去官网注册就好了。

配置package.json

{
  "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都没写,别人不知道怎么用也不敢用。下面一步步完善。

配置webpack

这里用的是最新版的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

添加单元测试

大家经常看到很多不错的项目都有Build Status,这就像一个证明可用性的证书,给人安全感和信任感,所以添加单元测试,还是很有必要的,同时也可以提高代码质量。先介绍需要用到的几个概念:

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 testyarn cover看看测试是否通过及测试覆盖率。

完善README.md

一个好的readme是决定用户用不用你项目的关键因素,所以要多花点心思,千万不能忽略。

  • 标题:直观的描述这个项目是干什么的。
  • 徽章:
    Build Status
    Coverage Status
    npm
    npm
    GitHub license
    分别表示是否构建成功、代码测试覆盖率、npm版本号、下载量、开源证书,看起来逼格满满有木有。推荐去shields io 添加,生成一次,之后会自动更新,不过需要等npm发布后才能搜到。
  • 配图:要让用户直观的看到这个组件长什么样,是否满足他的需求。
  • API介绍:不能让用户猜。
  • 使用示例:尽量降低使用门槛。

发布

#先登录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 。◕‿◕。

JavaScript Module 设计解析及总结

JS 模块化规范目前主要分为:CommonJS、AMD、CMD、UMD 和ES6。

ES6为名门正派,其它则为开发者草拟的野生规范

设计理念

CommonJS

//定义模块 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")的诞生。

AMD

由于模块化不是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

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。

UMD

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及import/export 的用法

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'
用法总结
  • import为编译时加载,而require为运行时加载,没法在编译时做“静态优化”。
  • import导入模块的属性或者方法是强绑定的,包括基础类型(即模块改变导入后的模块也改变);而 require 则是普通的值传递或者引用传递(即模块改变导入后的模块也不会改变)。
  • 使用export default时,对应的import语句不需要使用大括号;不使用export default时,对应的import语句需要使用大括号。

参考:阮一峰:模块的写法 阮一峰:AMD规范 阮一峰:Module 的语法

如何避免form表单onSubmit后页面自动刷新?

在搜索功能开发的时候,有时会碰到这种需求:点击输入法上的搜索按钮进行关键词搜索。这个需求可以拆分成两个需求:

  1. 输入法有“搜索”按钮;

  2. 点击“搜索”按钮执行搜索事件。

第一个需求很简单,设置inputtype="search"就可以。

<input type="search" placeholder="搜索关键词" />

第二个需求,可能很少接触,这个时候就需要借用form表单的submit提交。

<form onsubmit="handleSubmit()">
   <input type="search" placeholder="搜索关键词" />
</form>

重点来了

如果不加处理,就会触发form表单submit默认的页面刷新事件。我们必须手动消除form表单submit事件的页面默认刷新行为。下面推荐三种写法:

  1. 外部return false
<form onsubmit="handleSubmit();return false">
   <input type="search">
</form>
function handleSubmit() {
   ...
}
  1. 内部return false
<form onsubmit="handleSubmit()">
   <input type="search">
</form>
function handleSubmit() {
   ...
   return false
}
  1. 阻止默认行为preventDefault
<form onsubmit="handleSubmit(event)">
   <input type="search">
</form>
function handleSubmit(event) {
   e.preventDefault(event);
   ...
}

But

1、2两种写法在React均不支持,只能采用preventDefault了,写法如下:

handleSubmit(event){
    event.preventDefault();
    ...
}

render(){
    return (
        <form  onSubmit={this.handleSubmit}>
            <input type="search" placeholder="搜索关键词" />
        </form>
    )
}

不过,有个细节不知道大家注意到没,上面第三种写法的handleSubmitonsubmit里显示的传递了event,而这里并没有。是我多此一举还是有所考虑?大家思考下,我下次再说。

《疫苗之王》- 厉害了,我的锅!

由于微信公众号、微博等平台不可控因素,文章持续被删,故转载,希望尽己之力,能让更多人看到:github


2001年,东北一家国有疫苗公司悄无声息进行改制。多年后再回首,人们才明白其中意义。

那年的9月18日,上市公司长春高新旗下的长生生物迎来了两位新的股东——韩刚君和杜伟民。

韩刚君用1932万元买下了长生生物30%的股权,成为第二大股东;他和杜伟民的合资公司则成为了长生的小股东。

杜伟民是长生生物的销售总监。

这笔交易几乎没人注意到。长生生物被放到聚光灯下,是在两年后了。

2003年末,长春高新和长生生物的掌门人高俊芳把2000万打进公司账户,要将长生生物私有化。

算下来,高俊芳的出价是每股2.4元;而当时多位竞标者表示,他们愿意出3元/股的价格。

高俊芳很感谢他们的出价,然后拒绝了他们。

这引起了漫天质疑,有人把低价贱卖国有资产的举报信寄到了市政府。但仍然没有阻挡高俊芳成为长生生物第三大股东。

终于,高俊芳、韩刚君和杜伟民走到一起,他们手中握着长生生物的大半股份。

2007年,韩刚君把自己的股份卖给了高俊芳,帮助后者成为长生生物的绝对控制人。

十年后再回首,他们手中已经掌握了**疫苗的半壁江山——最大的乙肝疫苗企业、最大的流感疫苗企业、第二大水痘疫苗企业、第二和第四大狂犬病疫苗企业……

他们生产的疫苗,每天都源源不断,注入你和你孩子的身体中。

1

就在高俊芳顶着资本市场的唾骂,完成长生生物私有化的时候,韩刚君与杜伟民已经南下,他们盯上了刚刚拿到狂犬病疫苗生产资质的常州延申生物。

很快,韩刚君与杜伟民以2000万元拿下了常州延申90%的股份,将其改组成为江苏延申,韩刚君担任董事长。

杜伟民在加拿大远程完成了这一切。他这时已经拿到了加拿大绿卡,只要再待几年,他就能成为**人民老朋友白求恩大夫的老乡。

之后不到三年的时间里,韩刚君为江苏延申拿到了流感疫苗、气管炎疫苗、疖病疫苗的生产批文。江苏延申很快成为**最大的流感疫苗供应商和第四大狂犬病疫苗公司。

2007年10月,韩刚君和杜伟民已经准备好了上市资料。如果不是一次偶然的发现,江苏延申将会登陆资本市场。

2009年3月,大连金港迪安狂犬疫苗在抽检中被发现造假,食药监总局马上对狂犬疫苗生产企业进行突击检查,江苏延申被查出五批产品涉嫌造假。

食药监局发现,延申偷工减料、弄虚作假、逃避监管,疫苗抗原含量低于国家标准,达不到药效。

北大医学部的专家将注射失效的疫苗总结为两个字——杀人。

但这时,江苏延申的18万份疫苗已经流入21个省107个疾控中心,全部被注射进了病人体中。

江苏延申表示,我们无能为力。

没有召回、没有补偿。案件发生后,江苏延申因为生产、销售伪劣产品罪,被判处罚金三百万元,总经理和五名员工被判刑。

董事长韩刚君和另一位大股东却毫发无伤。

更诡异的是,江苏延申很快东山再起,仅仅半年之后,就获得了防疫部门160万人份甲流订单,价格超过亿元;不久又获得了甲流疫苗生产牌照。

就在调查组的眼皮子底下,杜伟民把这个生产假疫苗企业的股份全部转让出去,套现两亿元,顺利退出延申。

2

杜伟民的眼前是一片星辰大海。

根据一篇人物报道,2007年,杜伟民毅然变卖了加拿大的家产,放弃了入籍,带着妻儿回国了,重新投身疫苗领域。

“我熟悉这个行业,清楚**的生物产业是要发展的。而且我在国外越来越觉得,让疫苗技术掌握在**人自己的手上,关系到国家的生物安全”。

**真是一片化腐朽为神奇的土地。在海外迷失了自己,回国就对了。

不过,杜先生没有放弃加拿大绿卡和香港身份证。

2008年开始,杜伟民悄无声息地在产权交易所吃下了深圳老国企康泰生物的大部分股份,控制了这家**最大的乙肝疫苗生产商。

深圳康泰的乙肝疫苗技术,来自于美国人的人道主义援助。1989年9月,美国默克公司以微不足道的700万美元向**转让了全套工艺技术。

杜伟民把当年重组江苏延申的手法用在了康泰生物的重组上,开始为上市铺路。

疫苗企业上市,最大的障碍是产品种类单一,康泰多年来上市无功而返,就是因为只有乙肝疫苗这一种产品。

很快,杜先生通过收购北京民海生物,让康泰的产品多样化。2012年底到2013年,康泰自主研发的三款疫苗——Hib疫苗、麻风疫苗、四联疫苗获准生产,先后上市。

民海生物是怎么在短时间内获得三款产品的生产许可?

北京高级法院的一则审判书显示,2010年到2014年间,国家食品药品监督管理局药品审议中心副主任尹红章收受杜伟民47万元,为民海生物的药品申报审批事宜提供帮助。

除了加快新疫苗上市,杜伟民也让康泰的主打产品乙肝疫苗获得了新生。

当年杜伟民还在长生生物做销售时,**的疫苗市场基本被国有七大生物制品公司瓜分,互不侵犯。杜伟民瞅准时机,用拉低价格的方式抢占山头。

他在康泰复制了这一手段,在政府的招标中,康泰用低价摆平了一切竞争者。

乙肝疫苗单人份的出厂价格本来在15元左右,康泰直接报价6.9元,迅速拿下了国家免疫规划疫苗项目的大笔订单。对于竞标的研究所来说,这比它们的成本价还低,毕竟国家规定的出厂价都需要9.3元。

2013年12月,康泰和杜伟民迎来了最大危机。在十天时间里,共有8名新生儿在接种康泰的乙肝疫苗后死亡。

一个月后,食药总局和卫计委的调查显示,所有的婴儿死亡为偶合性死亡,疫苗质量没有问题,向康泰生物归还了生产证书。 风波过后,康泰安然上市,市值从杜伟民收购时的6亿元飙升到现在的400亿元。

3

高俊芳、韩刚君与杜伟民三位疫苗之王,有太多共同点。

他们对疫苗企业的控制和改造路径相似。瞄准的都是老牌疫苗企业,长生生物、延申生物和深圳康泰,背后都是**国有的生物制剂研究所。

他们以非常低廉的价格迅速入手,实现完全控股,然后在短时间内拿到多个疫苗生产牌照,为将来上市铺路。

最重要的一点,这些企业所属的有关部门似乎完全没有意识到疫苗生产牌照的价值——疫苗的毛利普遍在80%以上。

高俊芳买下长生生物时,企业估值为1.2亿元,2015年借壳上市时,市值为55亿元;
2008年杜伟民吃进深圳康泰时,企业估值为6亿元,2016年上市首日,市值达到138亿元;
江苏延申没能上市,不过韩刚君和杜伟民是以2222万元的估值入股的,不过就在他们重组的那三年,延申的净利润总额就达到8400万元。

兽爷的好友你包叔说:

都是九年义务教育,科研工作者怎么这么好骗。

康泰生物上市前的半年内,公司的股权在个人和机构之间进行了20多次倒手。

深交所曾经质疑其中是否存在利益输送和国资流失,要求公司说明转让的原因及合理性等,但康泰始终没有详细回答这个问题。

高俊芳则用同样的手法,把自己的儿子、老公、小姑子、外甥和侄女全部变成了长生生物的股东。

就像360安全卫士,安装后,你会发现有各种360软件出现在你的电脑里。

2017胡润百富榜中,杜伟民以73亿元的身价位列第559位,高俊芳家族以51亿位列第820位。

如果不是江苏延申的狂犬疫苗出了问题,韩刚君也该出现在这个名单上的。

疫苗之王们都起于草莽,没有人知道他们的钱从哪来的。

高俊芳入股长生生物的4000多万全部是自筹资金,当时上市公司的公告显示,她的月薪只有6000元。高俊芳说自己掏了200万,其余的钱一会说是亲友凑的,一会说是和银行贷款。

杜伟民和韩刚君之前分别是江西省卫生防疫站和河南开封龙亭区卫生防疫站的普通员工。下海不过四五年时间,成了疫苗行业最重要的资本推手。

都是天才。生子当如防疫员。

4

世界上最大的军火出口国是美俄英法中,联合国安理会的五个常任理事国。

**新闻事件最多的,也是这些疫苗之王们。

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);作者:兽爷)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.