dubinbin 发布的文章

JavaScript位移计算与计算精度的坑

前天迫于摸鱼,在leetcode做到了道题目,略有吐槽和体会,题目如下:

给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。

示例 1:

输入: num1 = "2", num2 = "3"

输出: "6"

示例 2:

输入: num1 = "123", num2 = "456"

输出: "56088"

说明:

1,num1 和 num2 的长度小于110。

2,num1 和 num2 只包含数字 0-9。

3,num1 和 num2 均不以零开头,除非是数字 0 本身。

4,不能使用任何标准库的大数类型(比如 BigInteger)或直接将输入转换为整数来处理。

题目链接:https://leetcode-cn.com/problems/multiply-strings/

先说结论,这题用JavaScript解答是深坑…..,而且你是跑不过最大的用例的,因为JavaScript精度存储机制就是如此。

首先,这个题目标注难度为中等,他需要你不是用直接转换数字进行相乘的方法得出,这里需要了解一个知识点,JavaScript位移计算,实际上我从对它不熟悉到理解使用看的不是JavaScript方面的介绍,而是这个https://www.cnblogs.com/outerspace/p/10846106.html,说到这里,大坑就是计算的结果,因为我上面说过坑的原因,即使你真的直接相乘,你可以在console或者node中试一下。

987654321 * 123456789

你大概会得到"121932631112635260",但是你肯定发现不对,这个相乘尾数不可能是0的,这里就会碰到JavaScript计算精度的坑了,以下部分学术型理论,可以战术跳过直接看下划线结论。

计算机的二进制实现和位数限制有些数无法有限表示。就像一些无理数不能有限表示,如 圆周率 3.1415926…,10 / 3 = 3.3333... 等。JavaScript 遵循 IEEE 754 规范,采用双精度存储(double precision),占用 64 bit。

根据按照 IEEE 754这个规范可找到说明:

IEEE 754 double-precision binary floating-point format: binary64[edit]

Double-precision binary floating-point is a commonly used format on PCs, due to its wider range over single-precision floating point, in spite of its performance and bandwidth cost. As with single-precision floating-point format, it lacks precision on integer numbers when compared with an integer format of the same size. It is commonly known simply as double. The IEEE 754 standard specifies a binary64 as having:

  • Sign bit: 1 bit // 符号位:1位
  • Exponent: 11 bits // 指数:11位
  • Significand precision: 53 bits (52 explicitly stored) // 有效精度:53位(显式存储52位)

那么按照这个换算,浮点数就没办法四舍五入表示了,总不能显示几百+的位数吧,这时候这时候浮点数只能模仿十进制进行四舍五入了,但是二进制只有 0 和 1 两个,于是变为 0 舍 1 入。这就是计算机中部分浮点数运算时出现误差,丢失精度的原因。

大整数的精度丢失和浮点数本质上是一样的,根据规范"显式存储52位", JS 中能精准表示的最大整数是 Math.pow(2, 53),即 9007199254740992。大于 9007199254740992 的可能会丢失精度。

你可以对其进行四则运算测试一下,得出结果非常不稳定,所以如果非要进行这种运算,建议进行分段拆分。

这道题主要让我了解到位移运算和JavaScript极限情况下计算精度的问题,当然基于现在的编译器已经优化过乘法计算,所以你真的按照位移去算出结果,代码维护性和性能也并不是最佳的,但是这其中涉及到的计算机基础知识对于理解很多JavaScript计算方面怪异表现有一定的帮助。

参考资料:

https://stackoverflow.com/questions/2373791/bitshift-in-javascript

https://en.wikipedia.org/wiki/Double-precision_floating-point_format

【闲扯】做技术应该早点搞懂自己的位置,及谈某些知识付费

某天晚上和前部门一哥在来福士喝茶,开始从工作扯起,逐渐不断升华到中层管理的陷阱和程序员要怎么提高自己的格局的问题,颇有感悟,大概总结一下论点。

【陷阱】

很多人做到中层,不上不下层次,尤其是技术人员,尤其容易陷入一种陷阱,"xx量级的访问量,xx级的数据",当你对外吹嘘这些数据,尤其可见在很多宣传上,"前xx高级经理,曾带领千万级....",久而久之,其实你已经无形中成为这个公司的服务型人员,就像ibm,oracle,你能做到这些是因为你的平台决定的,你已经被这个平台捆绑了,换了不一样的资源和平台,你可能并没有这么强大,假如哪天公司陷入困境,那么无疑这一类的“中层”的抗打击能力是最差的,因为如果你一旦将自己捆绑在这家公司平台之上,而你其实也是可以复刻的,抛除你管理的身份,你还没有普通码农好使,性价比高,那么无疑你是最危险的。也可以抬杠,"人家万一是公司一开始加入,非常熟悉那一套治理大公司的理论",但是国内这种级别的能有多少呢,换而言之这种大佬一般也不会出来吧。

技术人员的定位

我们应该承认在工作之初你会非常笃定,技术是唯一生产力,但是短则1年半载,长则几年,你会逐渐发现,在国内大部分公司会有一种氛围:“技术是最不值钱的",确实,在我们这种业务型的公司,会让人产生一种技术/程序员非常重要的错觉,尤其是一些闷头苦干的码农更是如此,但是如果你深入整个产品线,深入运营,商务的运作,你会惊讶,这帮人其实并没有闷头干代码,只是微信上聊聊天,打打电话偶尔做个文案啥的,看似清闲,可能拿着一份比你还高的薪水,对,这就是我们想讨论的,资源,其实大部分出去创业能成功的程序员并不是技术非常牛逼所以去创业,而是因为他们拥有资源,能够启动他们的事业,他们在微信群发消息,能够带来非常强大的复购和流量,这是我们作为关注技术但是不缊运营的程序员所不能及的,在国内大部分服务型公司里面,技术并不是衡量价值的全部,你需要有人购买,有人带量,有人传销式的帮你卖出去,这是企业生存的关键,所以即使你的技术再强大,只要“页面不挂”,那么业务可以运行,把你斗倒再找一个人维持业务运行就行了,所以开发不能只盯着自己的代码和技术,也要看看商务/运营在干什么,因为他们是直接产生利润的部门,而开发只是一个工具而已。

所谓知识付费

笔者在一家知识付费公司从事开发,但其实所谓知识付费也只是销售的高级说法,我习惯性认为我们是做课程的商城,我们并没有提供所谓知识付费/在线教育的长线辅导和支持以及真正落实教育的本质,我们只是想办法把某个课程售卖出去,而且最要命的是所有所谓打着知识付费的幌子的公司都有一个问题,大家集中点并不是知识传授上,毕竟公司要盈利,而大家为了想法设法卖出去可谓是花样百出,姿势齐全,比如你天天看到某些打着高大上包装的讲师,给你分析所谓致富和变强的秘诀,朋友,都2020年了,你觉得如果真的有这种赚钱 / 致富的诀窍,为什么要花几十块钱告诉你呢?排除掉他们真的那么牛逼这件事,他们能赚钱主要就算靠信息不对称,他们把诀窍秘诀告诉你,然后你来他的市场占用他的空间给他添堵?这种诀窍在任何时代都是传家宝,就算你爹教你都要给你留一手啊.....,你凭什么觉得一个陌生人花几十块钱就要把这些诀窍卖给你?这和以前书城里卖的成功学有区别吗?没有的。所以看到各种知识付费里面包装的各种大师,笑笑就好了,而公司为了盈利是会帮着所谓讲师包装,然后用那些不知道哪里拼凑来的内容做分销裂变,公司赚的很开心,消费者买到一堆垃圾,这不是割韭菜吗朋友?这个市场还能这样骗多久?

关于技术人员的定位,这里贴上我在知乎一篇文章,其中这几段击中了核心要点,与君共勉

作者:硅谷IT胖子 链接:https://www.zhihu.com/question/354217824/answer/883513247
来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处

技术人员地位问题华为不是做技术起家的。不是写代码或是做硬件,就是技术公司,我觉得很多人不懂这个基本道理。研发有很多人也的确很狂(包括我自己),摆不正自己的位置,在公司混是要吃亏的。

书生点说,美国的软件工程师向来把编程职位分两类:服务公司(Service)和技术公司(Tech)。这决定了研发人员是在Cost Center,还是Profit Center。别觉得这东西没用,知道的都知道很简单的道理,不知道的就很幼稚。

服务公司的典型是亚马逊、高盛、金融投资公司、Ebay等。研发人员在Cost Center,是“成本中心”,并不直接产生利润。比如亚马逊需要人写代码管理仓库(Fullfilment Center),没有了他们仓库当然无法运转,但利润不直接来自于他们而来自于卖货。所以,亚马逊码工地位就较低。金融等同理,创造直接价值的是交易员、投资人,而不是管理数据和模型的码工。

技术公司的典型是Google微软Facebook和LinkedIn等。可以说基本上一线大厂皆为技术公司,因为研发人员在Profit Center。比如一个ML Engineer提升了youtube的推荐算法,使得用户点击率上升,停留在youtube的时间增加了0.01%,自然提升了广告利润,这利润是直接来自技术提升的。

当然,现实中肯定没这么简单。亚马逊的AWS恐怕码工就是在Profit Center。但不管细节,是否在Profit Center,决定了研发人员的真正地位(不单单是工资)。

一般而言,技术公司里,普通码工为了项目的方向是敢跟自己经理以及其它职位吵架的:因为profit来自于码工,所以码工更多地有自主权。服务公司里面,码工一般只是一个执行角色,不会特别强势。

所以回到华为问题,华为的ProfitCenter里面有谁,是根本性问题。不要以为自己会写几行代码,收入高点,读几本破书,就是技术人才。在服务公司里,技术人员跟扫地的一样,都是“没的用时很可怕,用完了可以开掉”。

伪前端设想JWT构建用户体系【后端】

关键词:刷新JWT,更新,缓存

平台:数据库(mongodb, mysql),redis

首先在用户注册的时候,我们需要收集用户的账号信息 譬如 id + username + 乱七八糟mix salt 的组合用md5加密成一串用户初始token,和用户信息一起存入数据库,作为用户在平台唯一识别token(注意这个token和jwt发的token不是一回事)。

在用户注册登录的时候就把上述token作为cookie塞给用户浏览器,存在redis维护一份(这里看策略,一般可能2-4小时后失效),redis保存信息形式大概为token:{用户基本信息}
【下面的代码仅为思路展示,像很多sql和redis操作不能直接套用,仅供参考】

async login(*ctx*, *next*) {
​      try {
​        const {username, pwd} = await Util.treamentFormData(ctx.req)
​        const handlePsw = md5Pwd(pwd)
​        const getCheckUserInfo = await query(
​          `SELECT FROM users WHERE name = ${username} AND vkey = ${handlePsw}`
​        )
​        if (!getCheckUserInfo) {
​          return ctx.body =({code: 1, msg: '用户名或密码错误'})
​        } else {
​          const userInfo = {username: findOneRes.username, vkey: findOneRes.vkey}
​          const token = Util.setToken(userInfo)
​          const redis = initStone() 
​          redis.set(findOneRes.vkey, JSON.stringify(userInfo) , 'EX', 60 * 60 * 2) *//秒为单位 2个小时redis失效*
​          const CookieOpt = CookieConfig()
​          try {
​            ctx.cookies.set('_token', findOneRes.vkey, CookieOpt)
​          } catch(err) {
​            console.log(err)
​          }
​          ctx.response.body = ({code: 0, data: {...findOneRes._doc, token}})
​        }
​      } catch (e) {
​        ctx.response.body = e
​      }
  },

基于jwt有时效性我们前端调用的时候先查验一下jwt的有效时间,这里可以发起一个请求去获取最新的jwt,使用cookie作为凭据(所以这个cookie的时间可以设长一点,比如一个月),然后使用全局变量维护这个拿下来的jwt,当下一个接口请求的时候,会先去本地拿这个对象看看过没过期,如果过期了,去调这个获取jwt的接口再拿一次新的,这个接口返回新的token和用户个人信息

  *// 处理鉴权 不刷新cookie*
  async accessToken(*ctx*, *next*) {
​    try {
​        const getCookie = ctx.request.header.cookie
​       if (getCookie) {
​          *// redis.set('DD', 100, 'EX', 5) //秒为单位*
​          const parseCookie = Util.handleCookie(getCookie)
​          const getCookieToken = parseCookie['_token']
​          const redis = initStone() 
​          const result = await redis.get(`${getCookieToken}`)
​          if (result) {
​            const userInfo = JSON.parse(result)
​            const newToken = Util.setToken(userInfo)
​            ctx.body = ({code: 0, msg: 'success' , data: {token: newToken, userInfo}})
​          } else {
​            const getUserInfo = await query(
​              `SELECT FROM users WHERE vkey = ${handlePsw}`
​            ) 
​            if (getUserInfo) {
​              redis.set(getUserInfo.vkey, JSON.stringify(getUserInfo) , 'EX', 60 * 60 * 2) *//秒为单位 2个小时redis失效*
​              const newToken = Util.setToken(getUserInfo)
​              ctx.body = ({code: 0, msg: 'success' , data: {token: newToken, getUserInfo}})
​            } 
​          }
​       } else {
​          ctx.status = 401
​          ctx.body = ({code: -1, msg: 'fail'})
​       }
​    }  catch(e) {
​       console.log(e)
​    }
  }

这里通过cookie的toke去获取用户信息,如果redis上有这个信息(即_token = redis上的key "_token"),直接使用redis上的信息颁布新的jwt,如果没有,则通过这个token我们可以去查询数据里面个人信息,查到后再塞一次到redis,并且再返回个人信息和新的jwt给前端。

{
​    account: {
​        username: '',
                token: ''
​        …otherInfo,
​    }
}

一年前端工作-方法论-实践-思维

不知不觉自己从毕业已经从事了一年多的前端工作,当然如果加上实习期,那就更长了,一年中,有最初的兴奋和憧憬,也有迷茫和困惑,这里想要总结归纳一下这一年学到的一些程序的抽象层和思维和方法论上面的东西,谨以记录勉励自己前行。

这里首先致敬感谢老大乔总的鞭策和指导,如果说我在前几任老大学到怎么写代码,那么乔总教会的我是怎么从软件工程思维去思考一个问题,一种另类的学习方法和规划、设计思路的方法论。

模型层

在此前我一直待在一家996的公司,大家业务紧当然是能跑就行,最多加点"你看不看得懂没关系,反正老子写了"的注释,维护组件和数据异常蛋疼,上千行的组件非常常见。

当然呼应主题必须引入ts的故事

因为大佬强推Typescript接触到了Typescript,当然在一开始我是有点抵触的,"定义一个复杂些的东西好复杂啊,能不能不用啊"。但是后面发现确实加了这玩意之后,配合vscode各种类型提醒更有谱了,而且因为你定义好了所以你用a.b.c.d这种对象的时候不用反复比对这玩意之前是不是这么写的,并且线上确实很少发生"xxx underfunded,xxx is null"这种炸了的事情。"真香"

后来我发现大佬在处理一些复杂数据,例如多条件结算,多权限判断的时候使用了模型层的概念,我研究后发现这么干有如下好处

  • 视图更加纯粹地渲染视图,不带复杂恶心逻辑
  • 数据层处理好数据,接口变动无需去业务代码里面翻找修改,专心处理数据,这其实也是mvvm这类框架的灵魂。
  • 调用者可以无需知道model内层复杂的判断逻辑,调用方法即可,避免每个开发者去“review”这个方法所在的项目业务代码的痛苦过程
  • 可跨框架,跨项目使用这个model层,提高开发效率

先设计后实践

以前我拿到一个需求总是从图形入手再想这玩意大概用什么方法api可以搞定就动手了,这样子的思维存在一个缺陷,因为从图形入手我很容易在最后将他和view耦合过深, 抽离困难,或者卡住在某个地方没有思路继续不下去,后来我看到大佬在写一些程序的时候会在头部先写一些思维构想,譬如:"主要思路,抽取空闲通道,将弹幕插入xxxx,需要考虑**问题",然后再开始写代码,果真读他的代码结构清晰,和view耦合低,因为是原生js,所以甚至可以抽离出来给任何项目调用。

问题拆分组合分析能力

因为我们在做微信的一些业务,而微信生态里面发生了什么对于我们来说它就是一个黑盒,我们不能用常规的方式去排查它,而且网上也找不到任何确切相关的资料,而我们拿到一个问题的时候,总是觉得这个问题非常的庞大,很难拆分,不知道如何入手去排查他,而大佬通过自己一些经验告诉我们有时候一些问题不仅仅要靠常规经验,你必须动手用设置对照组,拆分条件,js不对拆css,拆加载顺序,模拟加载,多做实验,其实,软件工程也是跟科研一样,我们需要不停的实验不停的尝试,最终得到你自己的判断。

Service

你以为service只是后端的东西?前端也可以有,在项目中发现一些小工具可以开发出来公用,但是我们又不希望他只能在特定项目使用,具有通用性和容错性,譬如我们的前端授权服务,前端监控上报服务,我们都可以抽出sdk servide的概念,总结特点如下。

1,业务非耦合 - 不和业务耦合,即使跨项目也能使用能够通过npm,script直接引入,不依赖【业务依赖】

2,兼容 / 扩展性 - 可以通过独立发版进行拓展功能和升级,对不同项目没有js版本/依赖等要求

3,容错度 - 即使我们的服务炸了,也不会影响业务代码(搞出xxx is undefine等阻塞页面),或者最低影响

深层思考

万变不离其宗,其实很多人都在担忧前端技术更新日新月异好害怕,但是大佬用他的实践告诉我们,先不要害怕一个东西,先观察,但是不要先去看源码,先看我们用最简单的方法看能不能造出来,譬如api表现怎么样的。我们的思维定势一直是出了个新东西,先疯狂追一下新,然后学会怎么用。再看看能不能看懂源码,而大佬每次都能在我们说会用的时候给我们以降维打击。后期我学着这种思路去看了express,react-router, vuex, redux的一些实现,体会很深,有时候我们真的就卡在一个点,如果告诉你,react-router是基于context实现的props跨级传递和history api的一些简单应用(当然redux也是基于context),vuex的全局store数据驱动更新是因为它把store作为更高层级的"data"并利用了vue原本的数据对象观察的能力,我估计大部分人会说"哇,那这样我也知道啊",其实有时候就是这样的,我们的思维定势先设定我们很难去理解大神做的东西,所以我们缺了去思考"how it run"的动力。我们习惯于膜拜某些看源码的大佬,但是这种方式用大佬的说法是像高中解数学题,先看了答案再去想实现,这并不是最优的思维方式。

小技巧:使用useContext和useReducer构建小型redux

对于两个子组件间的通信,相信我们开发中并不少见,基于我们都不怎么喜欢redux的坚持…...其实我一直在用团队老大此前写的globalBus持续真香,我在上一篇文章也有讲到那玩意的原理和实现,但是老大说现在可以不用啦,新版hooks两个api联用,更香,于是我简单的实践了一番,发现…真香。

首先你需要在两个子组件之上的一层架设你的context chilFirst和ChildSecond是我建立的两个组件,我们将会从first组件发dipatch,在second组件展示变化的数据,这个场景开发中经常使用。

首先我们要建立context, 我们需要一个初始状态和一个变更state的状态管理器,咦,这怎么这么像…..

// Context.js
export const defaultState = {
    value: 0
}

export function reducer(state, action) {
    switch(action.type) {
        case 'ADD_NUM':
            return { ...state, value: state.value + 1 };
        case 'REDUCE_NUM':
            return { ...state, value: state.value - 1 };
        default: 
            throw new Error();
    }
}

Content.js ,这里我们需要显性声明Context.Provider 把数据传给包裹组件,这里轮到强大的useReducer出场了,按照官方要求,你需要把reducer(我们此前定义)和默认状态传入https://reactjs.org/docs/hooks-reference.html#usereducer,(这里小小利用hooks组件的状态,当我们改变了上层state后,其包裹的组件也会重新获得新的值)

// Content.js
import React, { useReducer, createContext } from 'react'
import { ChildFirst } from './ChildFirst'
import { ChildSecond } from './ChildSecond'
import { reducer, defaultState } from './reducer'

export const Context = createContext(null)

export function Content() {
    const [state, dispatch] = useReducer(reducer, defaultState)

    return (
        <Context.Provider value={{state, dispatch: dispatch}}>
            <ChildFirst/>
            <ChildSecond/>
        </Context.Provider>
    )
} 

组件First ,在这个组件我们dispatch发事件试试

// ChildFirst.js
import React, {useContext} from 'react'
import {Context} from './content'

export function ChildFirst() {
    const AppContext = useContext(Context)

    return (
        <div>
            <button onClick={ 
                 () => {
                AppContext.dispatch({
                    type: "ADD_NUM",
                    payload: {}
                  })
                }
            }>addNum</button>
            <button onClick={
                () => {
                AppContext.dispatch({
                    type: "REDUCE_NUM",
                    payload: {}
                    })  
                }
            }>reduceNum</button>
        </div>
    )
} 

组件Second

// ChildSecond.js
import React, {useContext} from 'react'
import {Context} from './content'

export function ChildSecond() {
    const AppContext = useContext(Context)

    return (
        <div>
            {AppContext.state.value + 's'}
        </div>
    )
} 

DEMO

我们发现在局部地区架设局部组件需要公用的context,不仅有利于我们组织结构的解耦,也可以避免我们一开始就要引入redux这种全局状态库或者中途引入带来的成本。本身作为一个应用需要公共管理的状态其实并不多,再加之团队开发素质不一管理不善的话,就很容易滥用redux,极大拖慢了整个应用的速度,所以学会这个技巧,可以优化组件间的状态管理和应用的轻便性能。

巧用Vue-Mixins实现跨级组件通信(Event-Bus)

众所周知,在我们现代开发中必不可少要用到Vue或者React, 那么我们一般父子通信一般会用Props, 虽然官方也会说两个同级兄弟组件你可以传到同级父组件然后分发,但是这样的效率实在令人捉急,层级一多你就蛋疼了(react官方出的context也是为了解决此类问题),既然都是单页应用,我们搞个事件监听和传递不就行了

在以前就有

element.addEventListener('click;', function(){})

首先我们需要借助发布订阅实现几个关键api,on / off / emit
他的原理也相当容易理解,我们维护一个事件队列,如果我们用一个key去增加/触发事件函数获得所需要的值,这样就完成了我们跨组件数据传递的需求

Event.js

class EventEmitter {
    constructor() {
        this.handersArr = {}
    }
    on(eventType, handle) {
        this.handersArr[eventType] = this.handersArr[eventType] || []
        this.handersArr[eventType].push(handle)
    }

    emit(eventType, ...args) {
        if (Array.isArray(this.handersArr[eventType])) {
            this.handersArr[eventType].forEach((item) => {
                item(...args)
            })
        }
    }

    off(eventType) {
        this.handersArr[eventType] && delete this.handersArr[eventType]
    }
}

export default EventEmitter

当然这样我们就完成了超级简单的事件管理器, 但是这里我们想把它用在vue上会需要做这么几个事情,在created接收其他组件的事件(如果有),同比react就是componentDidMount 然后在destoryed阶段(同比react就是componentWillUnmount)把这个事件摧毁,嗯,如果每次这么做就有点不够先进,而且和其他业务代码揉合在一起,不科学

那我们可以在Event.js搞点事情,

    bindVue(handlers) {
        const _this = this
        let handlersWithVue = {}
        return {
            created() {
               for (const [event, _handler] of Object.entries(handlers)) {
                   const handler = _handler.bind(this) // 把_handler的this绑定到vue调用环境中去
                   _this.on(event, handler)
                   handlersWithVue[event] =  handler
               } 
            },

            beforeDestroy() {
                _this.off(handlersWithVue)
            }
        }
    }

这里涉及到一个奇技淫巧,vue的mixins特征,简单暴力来说,它可以把你需要定制的vue的js部分的逻辑全部塞进你的业务.js里面,并起到作用,咦...这上面说的不就成了。

import EventBus from '../util/bus.js'
export default {
    mixins: [EventBus.bindVue({
         changeText(value) {  // 这里就是on方法啦
              this.msg = value
         }
    })],


   [emit:]  EventBus.emit({changeText: 666})

当然你会问React怎么实现,因为react真的是class,所以你拿到component真的就是标准对象,所以react的处理更方便点,直接把我们那个bindVue换成这个就成,react建议在constructor里面on监听(其实我们项目还有个hooks的实现23333),把我们需要的事件在componentDidMount执行,componentWillUnmount销毁

    bindComponent(component, handlers) {
        const didMount = component.componentDidMount
        const willUnmount = component.componentWillUnmount

        component.componentDidMount = () => {
            this.on(handlers)
            if (didMount) {
                return didMount.apply(component)
            }
        }

        component.componentWillUnmount = () => {
            this.off(handlers)
            if (willUnmount) {
                return willUnmount.apply(component)
            }
        }
    }

其实这里正好运用了JavaScript事件驱动和单线程的特点,比较复杂的是,你要注意this的指向,这里确实还是挺乱的,一不小心就不知道操作的是谁的this了。

前端也可以学会的Gitlab Ci前端自动部署

环境清单:

1,本机环境: Mac OSX

2,服务器环境:Centos7 ,nodejs,git

3,需要基础:一些基本服务器操作指令,git使用及了解gitlab平台基本使用

注:这里演示的都是基础能用的版本,实际生产肯定不能这么来的,根据大佬的建议一般来说这部署应该在docker环境配置,并且权限配置也很重要,而且runner机器和生产代码机器应该独立。

好了,前端自动部署,这个听起来如此高大上的东西是啥叻,我们为什么需要呢,对于众javaer来说jenkins肯定是不陌生的,这个原理颇为相似,但配置起来却简单的多(那不然我就写不了这个文章了),我们目前希望能够实现:

本地代码推到master => 触发钩子服务器自动执行脚本 => 拉代码,自动装依赖,打包,编译,上线

好了,确保你拥有环境清单里面的2,3,当然没有也没关系,又不是不能慢慢学......

1,第一步准备你的服务器公钥

如果你有的话它应该在 ~/.ssh目录下的id_rsa.pub,没有的话你可以创建一个

ssh-keygen -t rsa -C "youremail@example.com"

然后去gitlab setting的ssh key那里添加一下,这里是为了方便服务端去拉取git仓库代码,也是我们搞事情的关键。

2,在服务端安装gitlab runner

移除旧版本仓库(如果你之前搞过这个,如果没有,这步可以忽略):

sudo rm /etc/yum.repos.d/runner_gitlab-ci-multi-runner.repo

添加 GitLab's 官方仓库:

curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash

下载最新版 GitLab Runner:

sudo yum install gitlab-runner

3,注册runner

我们需要在这里和gitlab建立连接,命令行敲入

sudo gitlab-runner register 
// 注册 gitlab-ci-multi-runner
sudo gitlab-ci-multi-runner register

出来一系列对话如下

# 填写gitlab ci地址(见图1)

Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )

我这里用的是gitlab的库当然就是https://gitlab.com/啦,参数可看图1

------------------------------------------------------------

输入您获得的注册Runner的令牌:(见图1)

Please enter the gitlab-ci token for this runner

token,图一的位置有

------------------------------------------------------------

# 输入Runner的描述,你可以稍后在GitLab的UI中进行更改:

Please enter the gitlab-ci description for this runner[hostame]

my-runner (如果不是实际生产,随便写,只是标识作用)

------------------------------------------------------------

# 输入与Runner关联的标签,稍后可以在GitLab的UI中进行更改:

Please enter the gitlab-ci tagsforthisrunner (comma separated):

xxx(这里不填也行)

------------------------------------------------------------

选择Runner是否应该选择没有标签的作业,可以稍后在GitLab的UI中进行更改(默认为false):

Whether to run untagged jobs [true/false]:

[false]:true (暂时自己玩的话,可以直接空格调过)

------------------------------------------------------------

选择是否将Runner锁定到当前项目,稍后可以在GitLab的UI中进行更改。

Runner特定时有用(默认为true):

Whether to lock Runner to current project [true/false]:

[true]:true (暂时自己玩的话/不知道干啥用的,可以直接空格调过)

------------------------------------------------------------

# 输入Runner执行者:Please enter the executor: ssh, docker+machine, docker-ssh+machine, kubernetes, docker, parallels, virtualbox, docker-ssh, shell

shell(这里我们用较简单的shell命令来玩先)

步骤1,2信息可在gitalb -setting -ci(见图1)里面找到相关信息

图1

完成注册后我们的gitlab runner应该会被安排在/home/gitlab-runner 目录,至少我的是这样的。随后我们需要给gitlab-runner文件夹赋予权限,否则很有可能在跑runner的时候报permission denied,操作如下

sudo chown -R gitlab-runner:gitlab-runner /home/gitlab-runner
sudo chmod -R 777 /home/gitlab-runner

注意这里有个坑:远端拉下来的代码文件很可能在linxu下没有执行权限,我们可能会遇到permission denied的情况,这时候需要手动加权限 sudo chmod -R 777 /project_name ,然后使项目中的文件都可读写;再进行一个无关的修改,提交到远端分支,这样以后clone或者fetch下来的项目都是可读写的了。

一顿操作之后如果发现gitlab setting runner左边(例图2)这里有一个绿点证明我们刚刚挂载成功了,如果是红点和黑点,可能需要手动启动runner一下

sudo gitlab-ci-multi-runner start

图2

4,编写一个简单shell脚本去操作我们的前端文件

因为我极少接触shell,所以这里也是参考其他大佬的简单写法,因为我的前端文件在/www/wwwroot/下,所以这里执行的操作就是去这个目录自动拉代码并且编译,然后把这个文件放到/usr/local/bin/ ,放这里只是方便全局执行,其实也可能用其它方式去调用,任君选择,注意这玩意没后缀。

注:这里只是简单演示其基本原理,作为实际生产的ci,我们其实应该编译目录和线上目录是分开的,专门有一份文件进行这些拉/装/编/合的操作,最后进行替换才能减少用户被影响的时间,如果真的像如下这样搞,那用户等编译白屏这个过程2,3分钟,岂不日了狗。

deploy

# echo "更新代码...."

cd /root/../www/wwwroot/项目目录/

git pull

# build

echo "正在构建..."

npm install

npm run build

echo "构建成功...."

5,我们还需要一份.gitlab-ci.yml(这是建立连接的关键)

我们的项目一级目录添加一个.gitlab-ci.yml,里面内容大概就是我们runner定义的,意思是在我们master分支有变动时,触发钩子自动执行deploy脚本

https://gitlab.com/szdubinbin1/tryci/blob/master/.gitlab-ci.yml

stages:

  - deploy

deploy:

    stage: deploy

    script:

      - deploy

    only:

      - master

    tags:

      - my-runner

最后,测试一下

当我们的master代码发生变化(因为我懒,所以我用的是create-react-app脚手架做的测试),gitlab项目的ci/cd这里就可以看到我们执行的runner(图3)

图3

passed就是成功了,当然,光看这里是不行的,如果拉取git代码失败,它还是提示passed,原因猜测可能是它监测到shell脚本跑完就觉得ok的,这里还需要看一下jobs

我们可以发现在jobs(图4)它会反馈我们整个shell脚本、包括拉代码/装依赖/编译打包的过程,如果如下,具体项目可能不同,我这个是react,理论上vue打包成功也差不多这样,这时候我们刷新自己的项目地址就会发现修改生效了。

图4

1,跑脚本的时候,npm install可能会说权限不够,这时候只能去修改项目目录的权限

2,跑脚本的时候,git说没有ssh key,原因是我们刚开始添加的是root的ssh key ,gitlab runner可能用不了,解决方案:切到gitlab runner用户去添加一个ssh key

命令:su gitlab-runner => 添加ssh key

因为公司使用这个方案,不过当然比我这个高级的太多,我们重在实践了解一下前端自动化部署的大概流程,说不定哪天这玩意真的需要我们自己搭呢?折腾了这玩意一天,有一些莫名其妙的坑,不过好在都算解决了,真的感觉这玩意太高效率了,以后自己做项目就不用去服务器拉代码了。

利用Canvas和requestAnimationFrame API绘制圆形进度条动画

平台:React

相关技术: Canvas API , requestAnimationFrame API

刚入职不久,接到一个需求,关于重构课程列表,撇去蛋疼的看源码修改原有的显示逻辑之外,当时最令我印象深刻的是这个圆形进度条的制作过程。

在看到这个需求时,我首先想到能不能用css3进行实现,毕竟从性能来说css3是较好的,但是发现这样最后尾巴的小点没办法解决,而且技术难度貌似不小,最终部门小伙伴说你可以用canvas尝试一下,并给了个类似的demo参考,感恩~,我也趁机研究了一下canvas api的使用。

需求

首先我们拿到这个图案先拆分为4个部分,分别说外圆灰色细圈,外圆橘色细圈,内部橘色圈,以及尾巴的小点, 下面针对一些核心绘制API进行分析

这里讲一下比较蛋疼的最外侧的圆弧和那个点的开发。

最外侧的圆弧核心代码如下:(取context这种基础咱就不写了)

context.clearRect(0, 0,直径, 直径);
context.beginPath();   // 开始绘制 ,包工头说开始搬砖了
context.lineWidth = 任意粗细; // 定义绘制粗细 
context.strokeStyle = '定义画圈的颜色';
context.arc(直径 / 2,   直径 / 2,    直径 / 2 - 2*传入半径,   0,     传入需要的百分比数值 * 0.02 * Math.PI - 0.5 * Math.PI     false); // 重要,绘制圆形路径
context.stroke(); // 针对strokeStyle部分结束绘制
context.closePath(); //结束路径 - 包工头说下班了

这里主要讲一下这里运用context.arc的绘制思路,这里请大家跟我回忆w3school上关于这个API的用法定义

参考图(1)

前两个不用说,确定中心点,第三个通过传入一个半径,通过减去,切分的做法,"漏"出一个传入值的弧圈,可谓比较讨巧,原理看图好懂点。

传入值

而起始角和结束脚我们需要通过下面这个图(w3school参考图(2))说明,起始点0(-0.5 * Math.PI)

结束点:传入需要的百分比数值 0.02 Math.PI -0.5 * Math.PI ,先别急问0.02怎么来的,分析一下。

其实可以理解为-0.5 Math.PI(也就是1.5 Math.PI所在位置)就是起点,因为最大值也就是1.5*PI,所以这里的增值最多为-0.5 + x = 1.5   x = 2,那么  2/100 = 0.02份/1%,那么我们上面的公式就是这么来的。

w3school参考图(2)

那么上面的圆弧算是开发完毕了,我们来做一个更难的,那个点的跟踪计算。

首先先上核心代码

 context.beginPath();
 
 context.strokeStyle= "#FF9C00";
 
 context.lineWidth= 2;
 
 context.fillStyle= "#FF9C00";
 
 let radian= 传入进度 / 100 * 2 * Math.PI - 0.5 * Math.PI;
 
 let x= Math.cos(radian)* (大圆弧直径 / 2 - 2*裁去的半径)+ 直径 / 2(圆心x点);
 
 let y= Math.sin(radian)* (大圆弧直径 / 2 - 2*裁去的半径)+ 直径 / 2(圆心y点);;
 
 context.arc(x, y, 0.8 * r, 0, 2 * Math.PI, false);
 
 context.stroke();
 
 context.fill();
 
 context.closePath();

前四行为起手式级别,上面解释过,这里不再进行解释,我们主要分析第五行到第七行代码。

这里最难的是怎么找到这个点的位置(x,y)进行摆放,而绘制这个圆点方法如上如法炮制即可,已经比较简单了。

而我们知道数学公式里面求圆上的一点的公式

到了JS的Math.cos和Math.sin函数,我们查阅文档可以知道这两个函数中的传入值都是指的“弧度”而非“角度”,弧度的计算公式为: 2PI/360角度;那么你会说这样最高岂不是有2Math.PI ? 还记得我们上面的图吗,最高也就是1.5Math.PI,所以我们这里需要手动减去0.5Math.PI 。因为我们在最上面的需求已经可以得到弧度 :传入进度 / 100 2 Math.PI- 0.5 Math.PI的公式,所以通过它,我们可以得到一个类似xx Math.PI的弧度值。

如果要求算出(x1,y1)在(x0,y0)为圆心的圆上,其所在坐标,我们可以使用如下的公式得到圆点的位置。
x1 = x0 + r * cos(弧度值)
y1 = y0 + r * sin(弧度值)

那么到此我们终于可以绘制出静态的demo图了,慢着,你以为就结束了吗,naive

"UI大佬: 要不咱加个动效?"

好吧~向大佬势力屈服

那么怎么让它动起来呢?如果你仔细观察会发现我们刚刚实现的绘制过程,都是传入最终参数percent来让它完成绘制的,如果我每一次 precent+1,会有什么情况?不用你猜,对的,他会一次一次的绘制进度+1,这时候我突然想到"判断渲染的条件,动画,每次+1",脑海里浮现出面试复习时经常出现的那个巨长的API,requestAnimationFrame,不就是最适合这种场景么.
requestAnimationFrame API

实际实现思路就是,初始percent传0,通过判断是否达到percent,如果不是,percent+1继续再递归调用,而且他不会产生类似setTimeInterval定时器这种影响页面性能和事件队列的副作用。

一段实现代码

progressRender(context, length, R, r, percent,slogan){
 
          if(slogan!==0) {
 
         percent += 1;
 
         if (percent &lt; this.props.progress) {
 
               requestAnimationFrame(()=&gt; {
 
               this.progressRender(context, length, R, r, percent)
 
         })
 
      }
 
 }

最终我们能得到如下,ps:随便找的一个转gif的网站,感觉卡卡的,但是实际还好,比较流畅。

最终效果

通过这个小需求,不仅有点锻炼数学和逻辑能力,也有点考验我们对需求应变的机动准备,如果需求增加了我们要怎么灵活的实现出来,总之,继续加油吧~

Promise-All队列在Canvas应用上的小技巧

前几天在做一个抽奖转盘的时候,突然集中用到promise api所以记录并总结一二。

需求是做一个如下的转盘,转盘上的图片和文字是从后端字段返回的。

image

文字的绘制倒是问题不大,难点在于图片,众所周知canvas的context.drawImage(img element)所绘制的图片需要是已经保证onload的img对象,当时我的第一想法是拿到后端图片数组之后依次onload它,并塞进一个新的数组,数组长度 = 后端数组长度,再调用绘制api不就行了,伪代码如下


const newArr = []

for (let i = 0; i <arr.length; i++) {

      arr[i].onload  = () => {

        newArr.push(arr[i])

        if (newArr.length === 后端图片数组.length) {

              调用方法绘制转盘方法

        }

      }

}

至少一开始我也觉得没问题的,但是发现只要你清空缓存(第一次进入)图片顺序就不对了,抽奖的顺序不对那可不是小事情,于是乎我console了一下如上代码的输出数组的图片顺序,我发现他并不会一个一个按照顺序的塞入数组,想到for和onload的执行原理,它onload完了才会给你这个回调,那确实是不可靠的。

后来大佬看到我在纠结于此,便提出你可以参考一下我的做法随手便甩给我一个仓库地址,我看到第一眼promise all这个api我就知道了大概做法(也就是说其实我并没有看大佬后面的具体实现2333,不过我猜想应该差不多)。这个api第一次用还是当时为了应付面试所以临时学习的,事实上工作中前端业务所用到的场景并不多,一般在后端nodejs上可能对于队列之间有严格前后顺序依赖关系的业务会比较有帮助,一般前端需求promise单api即可完成所以使用不多,但恰好canvas这个drawimage需要的就是加载完的图片,而且需要顺序正确。

下面是具体代码,在preloadImage我们把img的onload这一过程作为promise对象存进队列,也就是说其实你打印this.renderList 会得到[promise, promise,promise,promise,....]这样一个数组, 这个promise对象返回值就是我们已经onload成功的img对象,再通过promise all,它等待所有返回完成再执行后面的绘制过程,虽然这个过程可能会有一点点滞后但是对于这种强顺序关系并且需要已经加载完成的事件,这个api就发挥的淋漓精致了。


  async renderTheWheelImage(params) {    // params : ['图片src', '图片src', ....]

            for (let i = 0; i < params.length; i++) {

                await this.preloadImage(params[i])

            }

            Promise.all(this.renderList).then((res) => {

               // 绘制转盘的方法(res)

            })

        },

        preloadImage(item) {

            this.renderList.push(new Promise((resolve, reject) => {

                const newImage = new Image()

                newImage.onload = () => {

                    resolve(newImage)

                }

                newImage.onerror = reject

                newImage.src = item

            }))

        },
preView