ccfnever

ccfnever 查看完整档案

填写现居城市  |  填写毕业院校魅族科技  |  前端开发 编辑 www.ccfnever.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

ccfnever 关注了专栏 · 2020-11-25

有赞美业前端团队

关注 2794

ccfnever 赞了文章 · 2020-11-10

使用sequelize操作数据库 时间格式化

用sequelize去取数据库中date类型的时间,得到的是:

2019-04-24T08:57:58.000Z

这种样子。

在网上看了一些解决方法都是说用moment这个库去格式化时间,还有一种方法是直接修改sequelizi配置:

const sequelize = new Sequelize(dbName, user, password, {
  dialect: 'mysql',
  host,
  port,
  logging: false,
  timezone: '+08:00',
  define: {
    timestamps: true,
    paranoid: true,
    createdAt:'created_at',
    updatedAt:'updated_at',
    deletedAt:'deleted_at',
    underscored: true
  },
  // 添加这个配置 
  dialectOptions: {
    dateStrings: true,
    typeCast: true
  }
})

这种方法我觉得更好一些。

配置之后就可以给前端返回正确的时间了。

查看原文

赞 3 收藏 0 评论 0

ccfnever 发布了文章 · 2020-08-13

extra_hosts 无效?如何 在docker build 时修改 hosts

公司项目用的是 docker 环境部署,就遇到一个需要在 docker image 构建阶段修改 hosts 的情况。

比如这样的 dcoker-compose.yml 和 Dockerfile 组合

#dcoker-compose.yml

version: "3.3"

services:
  app-web:
    build: ./
    image: app-web
    ports:
      - 4600:4600
    container_name: "app-web"
#Dockerfile

FROM node:latest

WORKDIR /usr/src/app/
USER root
COPY ./ ./

RUN npm run install && npm run build

CMD ["npm", "run", "start-dev"]

npm run install需要依赖自定义 hosts 的时候,这就尴尬了。

查看 docker 文档,其中提到在 docker-compose.yml 上可以设定 extra_hosts 以拓展 hosts。

test2:
  build: local/test
  extra_hosts:
    - raw.githubusercontent.com:199.232.68.133

但是我怎么测试,都不生效。很神奇 -。-

可以在 Dockerfile 里测试下输出

RUN echo '199.232.68.1 aaa.bb.com' >> /etc/hosts
RUN cat /etc/hosts # 文件被重置,上一行的修改不生效

要解决这个问题,有两个方法:

一、 把hosts修改和安装写在一个 shell 脚本,ADD 到容器,然后执行。

#!/bin/bash
echo '199.232.68.133 aaa.bbb.com' >> /etc/hosts
cat /etc/hosts #生效
npm install

二、 使用 && 操作符在 Dockerfile 同一条 RUN 命令执行

RUN echo '199.232.68.1 aaa.bb.com' >> /etc/hosts && cat /etc/hosts # hosts生效
RUN cat /etc/hosts # 文件被重置,hosts不生效
查看原文

赞 0 收藏 0 评论 0

ccfnever 发布了文章 · 2020-05-05

效率噌噌噌 - 搭建高效好用的前端开发环境(外设篇)

这几天换设备,就跟搬新家似的,难免又要布置一番,加上疫情爆发,凸显了在家办公的重要性,故借此机会做个分享与记录。

系列篇幅较长,我会分次更新。

本文环境基于 Mac 系统,分享我的外设和常用的软件、配置,包括:

  • 外设(笔记本、显示器、鼠标键盘、电脑椅)
  • 效率软件
  • 常用快捷键
  • 前端必备环境、工具
  • chrome 插件
  • vscode 配置

啥??? 你用 window ?

听哥一句,赶紧拿起手机买个 macbook pro 去,mac的好这里就不多说了,谁用谁知道。

--

瞄一眼居家办公房间

一眼看去简约整洁(机智的我已经把零食纸巾袜子啥的都收了😏)

模块也很简单:

  • 2015 款 macbook pro笔记本 x 1
  • 27 寸 4K显示器 x2
  • 显示器支架 x 1
  • 入门级人体工学椅子 x1
  • Filco 机械键盘茶轴 x1
  • apple 妙控鼠标 x1
  • 恐龙抱枕 x1

Macbook Pro

我的是 2015 款的 macbook pro,到如今 2020 年,已经奋战了快 5 年,依然老当益壮啊。性能各方面都能满足。

哪怕是集成显卡 Intel Iris Pro 1536 MB, 玩 dota2 也无压力。
咱们写写前端就更不在话下了。😋

所以说,如果你是刚毕业或者是学校里的学生,手头木有足够 money,可以买个二手 macbook pro,也是完全够用的。

但mac笔记本用久了,会有个通病。
那就是电~池~鼓~包

严重的时候触控板都按不下去。

拆开看是这样

不过这岂能难倒动手能力超强的码农[邪魅一笑]

某宝 220 元入手了新电池,换上后果断复活了。
鼓包没了,续航回来了,感觉可以再战 2 年。

外接显示器

mac 笔记本毕竟只有 15 寸,用笔记本屏幕偶尔带出去改改 bug 啥的,是够用,但要用的爽还是要连外屏。

关于外接显示屏的选择,我也是做过不少攻略,最终选择了 2 个 LG 的27寸 4K 双屏幕的搭配。型号为 LG 27ud68。

理由如下:

  • 【分辨率】 1080p是不可能了,这辈子都不可能,至少4k。
  • 【接口】 2015 款 macbook pro 支持雷电2 接口转 DP高清,可以做到 4k 60hz,而且有两个接口。
  • 【尺寸】 如今是大屏时代了,但办公座位还是离屏幕很近的,27寸单屏太小,32寸双屏太大。所以27寸双屏比较适中。
  • 【价格】 LG 的屏幕有口皆碑,价格也合适。当初618某东2千多买入一个,用了2年爽歪歪,于是后来闲鱼又1千多买了一个,双屏双倍快乐。

当然,显示器支架也很重要。

它可以让你桌面走线更整洁,还可以让你把屏幕竖起。

支架某宝174购入。

入门级人体工学椅子

话说,椅子不仅仅是生产力,说是生命力也不为过啊。
经常码代码一搞就是几小时的偶们,椅子不好容易腰膝酸软、肩周炎、颈椎病、胳膊酸痛甚至菊花疼。
以上问题常年积累,就算不短命,也会让你晚年潇洒不起来。

但考虑到在家办公的时间并不长。(码农,公司才是真正的家?)
我也没必要选特别贵的椅子。(主要还是贫穷😂)

选了好久,翻遍知乎,最后选了国产的 金豪b。包邮入手1220。

看起来不大,但是贼重。。。
安装很方便,用起来坐感还行,腰部支撑也挺好。但是久坐菊花疼啊。。。
这是为神马?难道是网布的问题?😨
于是不得以,买了个坐垫。效果还不错

鼠标键盘

对咱们码农来说,键盘鼠标就是锄头耙子啊。

机械键盘是必须的了,我选了 Filco 机械键盘茶轴。我也用过青轴和红轴,个人还是喜欢茶轴,青轴太脆太吵,红轴太软太直。
青轴吃灰了,红轴送人了,家里和公司用的都是 大 F 的茶轴。
这里要吐槽下 Filco 键盘的蓝牙,太垃圾了。经常断连,还有延迟。 于是我虽然买的蓝牙版,大部分时间还是连着 usb,当做有线的用。

某宝 1千入

鼠标的话,苹果鼠还是很好用的,有各种手势,还可以很方便的左右滑动。

恐龙抱枕

是滴,憨厚老实的小恐龙抱枕。

你肯定想问,这个萌萌哒的抱枕,跟前端编程有啥关系?

别说,还真有些用。

当你独自编程,或是在寂寞中学习的时候,看下这呆萌的微笑,可以获取一丝陪伴感和宽慰。。。

当你遇到傻逼需求或泉水被虐,心中充满愤怒的时候,抓起小恐龙一顿胖揍,可以释放你的怒火,平静你的心灵。。。

但对编程而言,最最重要的是它可以帮你 debug 啊。

比如每个程序员老司机都有过好多神奇的经历,就是遇到一个不明所以的 bug,原因可能是极其简单。

比如写错一个变量名、hosts没切换,配置忘记改、缓存没清除、执行权限等问题。

但你就是陷入思维盲区,可能你折腾大半天,仍然百思不得其解。

而这时候如果让一个同事过来瞧瞧,他可能一眼就看出了问题所在。思维惯性就是这么可怕 😂

好了,家里没别人的时候小恐龙就是用来充当你同事的。

这时候要充分发挥精神分裂思想。把小恐龙想象成你同事,然后问它咋回事呀。然后从由头把逻辑捋一遍。

可能很快,就会发现问题所在了。😎

当然,除了小恐龙,精分对象可以是任意物体,有角色属性的最好不过了,比如漂亮滴手办

查看原文

赞 11 收藏 3 评论 7

ccfnever 赞了文章 · 2020-01-01

如何设计redux state结构

为什么使用redux

使用react构建大型应用,势必会面临状态管理的问题,redux是常用的一种状态管理库,我们会因为各种原因而需要使用它。

  1. 不同的组件可能会使用相同的数据,使用redux能更好的复用数据和保持数据的同步
  2. react中子组件访问父组件的数据只能通过props层层传递,使用redux可以轻松的访问到想要的数据
  3. 全局的state可以很容易的进行数据持久化,方便下次启动app时获得初始state
  4. dev tools提供状态快照回溯的功能,方便问题的排查

但并不是所有的state都要交给redux管理,当某个状态数据只被一个组件依赖或影响,且在切换路由再次返回到当前页面不需要保留操作状态时,我们是没有必要使用redux的,用组件内部state足以。例如下拉框的显示与关闭。

常见的状态类型

react应用中我们会定义很多state,state最终也都是为页面展示服务的,根据数据的来源、影响的范围大致可以将前端state归为以下三类:

Domain data: 一般可以理解为从服务器端获取的数据,比如帖子列表数据、评论数据等。它们可能被应用的多个地方用到,前端需要关注的是与后端的数据同步、提交等等。

UI state: 决定当前UI如何展示的状态,比如一个弹窗的开闭,下拉菜单是否打开,往往聚焦于某个组件内部,状态之间可以相互独立,也可能多个状态共同决定一个UI展示,这也是UI state管理的难点。

App state: App级的状态,例如当前是否有请求正在loading、某个联系人被选中、当前的路由信息等可能被多个组件共同使用到状态。

如何设计state结构

在使用redux的过程中,我们都会使用modules的方式,将我们的reducers拆分到不同的文件当中,通常会遵循高内聚、方便使用的原则,按某个功能模块、页面来划分。那对于某个reducer文件,如何设计state结构能更方便我们管理数据呢,下面列出几种常见的方式:

1.将api返回的数据直接放入state

这种方式大多会出现在列表的展示上,如帖子列表页,因为后台接口返回的数据通常与列表的展示结构基本一致,可以直接使用。

2.以页面UI来设计state结构

如下面的页面,分为三个section,对应开户中、即将流失、已提交审核三种不同的数据类型。
示例
因为页面是展示性的没有太多的交互,所以我们完全可以根据页面UI来设计如下的结构:

tabData: {
    opening: [{
        userId: "6332",
        mobile: "1858849****",
        name: "test1",
        ...
    }, ...],
    missing: [],
    commit: [{
        userId: "6333",
        mobile: "1858849****",
        name: "test2",
        ...
    }, ... ]
}

这样设计比较方便我们将state映射到页面,拉取更多数据只需要将新数据简单contact进对应的数组即可。对于简单页面,这样是可行的。

3.State范式化(normalize)

很多情况下,处理的数据都是嵌套或互相关联的。例如,一个群列表,由很多群组成,每个群又包含很多个用户,一个用户可以加入多个不同的群。这种类型的数据,我们可以方便用如下结构表示:

const Groups = [
    {
        id: 'group1',
        groupName: '连线电商',
        groupMembers: [
            {
                id: 'user1',
                name: '张三',
                dept: '电商部'
            },
            {
                id: 'user2',
                name: '李四',
                dept: '电商部'
            },
        ]
    },
    {
        id: 'group2',
        groupName: '连线资管',
        groupMembers: [
            {
                id: 'user1',
                name: '张三',
                dept: '电商部'
            },
            {
                id: 'user3',
                name: '王五',
                dept: '电商部'
            },
        ]
    }
]

这种方式,对界面展示很友好,展示群列表,我们只需遍历Groups数组,展示某个群成员列表,只需遍历相应索引的数据Groups[index],展示某个群成员的数据,继续索引到对应的成员数据GroupsgroupIndex即可。
但是这种方式有一些问题:

  1. 存在很多重复数据,当某个群成员信息更新的时候,想要在不同的群之间进行同步比较麻烦。
  2. 嵌套过深,导致reducer逻辑复杂,修改深层的属性会导致代码臃肿,空指针的问题
  3. redux中需要遵循不可变更新模式,更新属性往往需要更新组件树的祖先,产生新的引用,这会导致跟修改数据无关的组件也要重新render。

为了避免上面的问题,我们可以借鉴数据库存储数据的方式,设计出类似的范式化的state,范式化的数据遵循下面几个原则:

  • 不同类型的数据,都以“数据表”的形式存储在state中
  • “数据表” 中的每一项条目都以对象的形式存储,对象以唯一性的ID作为key,条目本身作为value。
  • 任何对单个条目的引用都应该根据存储条目的 ID 来索引完成。
  • 数据的顺序通过ID数组表示。

上面的示例范式化之后如下:

{
    groups: {
        byIds: {
            group1: {
                id: 'group1',
                groupName: '连线电商',
                groupMembers: ['user1', 'user2']
            },
            group2: {
                id: 'group2',
                groupName: '连线资管',
                groupMembers: ['user1', 'user3']
            }
        },
        allIds: ['group1', 'group2']
    },
    members: {
        byIds: {
            user1: {
                id: 'user1',
                name: '张三',
                dept: '电商部'
            },
            user2: {
                id: 'user2',
                name: '李四',
                dept: '电商部'
            },
            user3: {
                id: 'user3',
                name: '王五',
                dept: '电商部'
            }
        },
        allIds: []
    }
}

与原来的数据相比有如下改进:

  1. 因为数据是扁平的,且只被定义在一个地方,更方便数据更新
  2. 检索或者更新给定数据项的逻辑变得简单与一致。给定一个数据项的 type 和 ID,不必嵌套引用其他对象而是通过几个简单的步骤就能查找到它。
  3. 每个数据类型都是唯一的,像用户信息这样的更新仅仅需要状态树中 “members > byId > user” 这部分的复制。这也就意味着在 UI 中只有数据发生变化的一部分才会发生更新。与之前的不同的是,之前嵌套形式的结构需要更新整个 groupMembers数组,以及整个 groups数组。这样就会让不必要的组件也再次重新渲染。

通常我们接口返回的数据都是嵌套形式的,要将数据范式化,我们可以使用Normalizr这个库来辅助。
当然这样做之前我们最好问自己,我是否需要频繁的遍历数据,是否需要快速的访问某一项数据,是否需要频繁更新同步数据。

更进一步

对于这些关系数据,我们可以统一放到entities中进行管理,这样root state,看起来像这样:

{
    simpleDomainData1: {....},
    simpleDomainData2: {....}
    entities : {
        entityType1 : {byId: {}, allIds},
        entityType2 : {....}
    }
    ui : {
        uiSection1 : {....},
        uiSection2 : {....}
    }
}

其实上面的entities并不够纯粹,因为其中包含了关联关系(group里面包含了groupMembers的信息),也包含了列表的顺序信息(如每个实体的allIds属性)。更进一步,我们可以将这些信息剥离出来,让我们的entities更加简单,扁平。

{
    entities: {
        groups: {
            group1: {
                id: 'group1',
                groupName: '连线电商',
            },
            group2: {
                id: 'group2',
                groupName: '连线资管',
            }
        },
        members: {
            user1: {
                id: 'user1',
                name: '张三',
                dept: '电商部'
            },
            user2: {
                id: 'user2',
                name: '李四',
                dept: '电商部'
            },
            user3: {
                id: 'user3',
                name: '王五',
                dept: '电商部'
            }
        }
    },
    
    groups: {
        gourpIds: ['group1', 'group2'],
        groupMembers: {
            group1: ['user1', 'user2'],
            group2: ['user2', 'user3']
        }
    }
}

这样我们在更新entity信息的时候,只需操作对应entity就可以了,添加新的entity时则需要在对应的对象如entities[group]中添加group对象,在groups[groupIds]中添加对应的关联关系。

enetities.js

const ADD_GROUP = 'entities/addGroup';
const UPDATE_GROUP = 'entities/updateGroup';
const ADD_MEMBER = 'entites/addMember';
const UPDATE_MEMBER = 'entites/updateMember';

export const addGroup = entity => ({
    type: ADD_GROUP,
    payload: {[entity.id]: entity}
})

export const updateGroup = entity => ({
  type: UPDATE_GROUP,
  payload: {[entity.id]: entity}
})

export const addMember = member => ({
  type: ADD_MEMBER,
  payload: {[member.id]: member}
})

export const updateMember = member => ({
  type: UPDATE_MEMBER,
  payload: {[member.id]: member}
})

_addGroup(state, action) {
  return state.set('groups', state.groups.merge(action.payload));
}

_addMember(state, action) {
  return state.set('members', state.members.merge(action.payload));
}

_updateGroup(state, action) {
  return state.set('groups', state.groups.merge(action.payload, {deep: true}));
}

_updateMember(state, action) {
  return state.set('members', state.members.merge(action.payload, {deep: true}))
}

const initialState = Immutable({
  groups: {},
  members: {}
})

export default function entities(state = initialState, action) {
  let type = action.type;

  switch (type) {
    case ADD_GROUP:
      return _addGroup(state, action);
    case UPDATE_GROUP:
      return _updateGroup(state, action);
    case ADD_MEMBER:
      return _addMember(state, action);
    case UPDATE_MEMBER:
      return _updateMember(state, action);
    default:
      return state;
  }
}

可以看到,因为entity的结构大致相同,所以更新起来很多逻辑是差不多的,所以这里可以进一步提取公用函数,在payload里面加入要更新的key值。

export const addGroup = entity => ({
  type: ADD_GROUP,
  payload: {data: {[entity.id]: entity}, key: 'groups'}
})

export const updateGroup = entity => ({
  type: UPDATE_GROUP,
  payload: {data: {[entity.id]: entity}, key: 'groups'}
})

export const addMember = member => ({
  type: ADD_MEMBER,
  payload: {data: {[member.id]: member}, key: 'members'}
})

export const updateMember = member => ({
  type: UPDATE_MEMBER,
  payload: {data: {[member.id]: member}, key: 'members'}
})

function normalAddReducer(state, action) {
  let payload = action.payload;
  if (payload && payload.key) {
    let {key, data} = payload;
    return state.set(key, state[key].merge(data));
  }
  return state;
}

function normalUpdateReducer(state, action) {
  if (payload && payload.key) {
    let {key, data} = payload;
    return state.set(key, state[key].merge(data, {deep: true}));
  }
}

export default function entities(state = initialState, action) {
  let type = action.type;

  switch (type) {
    case ADD_GROUP:
    case ADD_MEMBER:
      return normalAddReducer(state, action);
    case UPDATE_GROUP:    
    case UPDATE_MEMBER:
      return normalUpdateReducer(state, action);
    default:
      return state;
  }
}

将loading状态抽离到根reducer中,统一管理

在请求接口时,通常会dispatch loading状态,通常我们会在某个接口请求的reducer里面来处理响应的loading状态,这会使loading逻辑到处都是。其实我们可以将loading状态作为根reducer的一部分,单独管理,这样就可以复用响应的逻辑。

const SET_LOADING = 'SET_LOADING';

export const LOADINGMAP = {
  groupsLoading: 'groupsLoading',
  memberLoading: 'memberLoading'
}

const initialLoadingState = Immutable({
  [LOADINGMAP.groupsLoading]: false,
  [LOADINGMAP.memberLoading]: false,
});

const loadingReducer = (state = initialLoadingState, action) => {
  const { type, payload } = action;
  if (type === SET_LOADING) {
    return state.set(key, payload.loading);
  } else {
    return state;
  }
}

const setLoading = (scope, loading) => {
  return {
    type: SET_LOADING,
    payload: {
      key: scope,
      loading,
    },
  };
}

// 使用的时候
store.dispatch(setLoading(LOADINGMAP.groupsLoading, true));

这样当需要添加新的loading状态的时候,只需要在LOADINGMAP和initialLoadingState添加相应的loading type即可。
也可以参考dva的实现方式,它也是将loading存储在根reducer,并且是根据model的namespace作为区分,

dva loading

它方便的地方在于将更新loading状态的逻辑被提取到plugin中,用户不需要手动编写更新loading的逻辑,只需要在用到时候使用state即可。plugin的代码也很简单,就是在钩子函数中拦截副作用。

function onEffect(effect, { put }, model, actionType) {
    const { namespace } = model;

  return function*(...args) {
    yield put({ type: SHOW, payload: { namespace, actionType } });
    yield effect(...args);
    yield put({ type: HIDE, payload: { namespace, actionType } });
  };
}

其他

对于web端应用,我们无法控制用户的操作路径,很可能用户在直接访问某个页面的时候,我们store中并没有准备好数据,这可能会导致一些问题,所以有人建议以page为单位划分store,舍弃掉部分多页面共享state的好处,具体可以参考这篇文章,其中提到在视图之间共享state要谨慎,其实这也反映出我们在思考是否要共享某个state时,思考如下几个问题:

  1. 有多少页面会使用到该数据
  2. 每个页面是否需要单独的数据副本
  3. 改动数据的频率怎么样

参考文章

https://www.zhihu.com/questio...
https://segmentfault.com/a/11...
https://hackernoon.com/shape-...
https://medium.com/@dan_abram...
https://medium.com/@fastphras...
https://juejin.im/post/59a16e...
http://cn.redux.js.org/docs/r...
https://redux.js.org/recipes/...

查看原文

赞 15 收藏 10 评论 3

ccfnever 关注了用户 · 2019-10-16

kaiye @kaiye

公众号「猫哥学前班」

关注 5

ccfnever 赞了文章 · 2019-10-15

如何维护老旧代码

我们在平时的工作中,总是会遇到老旧的系统以及老旧陈的代码。他们是业务长年累月的积累,以及因为是三、四年前的技术选型造成的系统架构的不合理以及繁琐的代码。维护这些代码总是很头疼,程序员遇到这样的代码总是一边骂娘一边憋屈的维护这,维护这些代码选择的方式并不多:

  • 1.推倒重来,从设计视觉到前端代码甚至后端接口和逻辑全是新的。

  • 2.修旧如旧,既然这么烂了我们就让他更烂吧,反正已经这么恶心了。。。

  • 3.新的逻辑启用新的架构和技术选型,尽量减少对旧的代码的依赖和旧的逻辑的修改

一般来说:第一种选择总是最好的,程序员最喜欢的,重构么,大家都喜欢。不过也是工作量最繁重的,它需要从上到下梳理清楚现有业务的所有逻辑,视觉稿,交互稿,文案梳理,逻辑处理,后端接口逻辑以及测试需要回归所有的case。当一个系统已经被三四个人维护过,产品经理换了四五茬,后端开发也换了三四茬,文档不健全,梳理这样的系统里的一个模块都是需要一两周的,一个系统有十来个这样的模块。。想想就是一个巨量的工作。再加上重构。。。总是会遇到各种阻力的。。。

第二种选择:修旧如旧,也会有人这么干的,“破窗户理论”嘛,这种方案不发表评论。

第三种方案,算是一种折中的选择,维护旧的系统大部分情况下是修修补补,偶尔添加一些新功能模块。
大致示例如下:

我就在想,能不能通过稍微优雅一点的方式来维护这些老旧代码呢?比如旧的逻辑代码我们尽量少的改动,对于新加模块我们就启用新的代码和技术选型,这样我们虽然在新旧两种代码中穿梭,不过我们大部分时间都在新的技术选型和架构里维护代码。也可以逐步的梳理熟悉流程,慢慢的把旧的逻辑迁移过来。弊端就是:需要维护两套代码,理解两套技术选型。好处就是随着新增业务模块,新的代码会越来越多,慢慢的就把旧的代码废弃了。

那么问题就来了:

新的代码如何和旧的代码解耦?

新代码我们当然是用新仓库,新选择,新打包工具。。。比如:我现在维护的一个系统是四五年前的一直正常的运行,代码选项是kissy,模块依赖也是kissy的那一套技术体系,没有通用的UI控件,打包用的简单的压缩,代码里还兼容这IE6,7,8。而实际上现在这套系统只跑在chrome上。在现在的视角看,有些东西就可以舍弃。

新的技术选型是:webpack,vue,ES6之类的,当然这些不是最主要的,最主要的是如何解耦新旧业务逻辑,如何在AB模块之间插入一个A1模块。并且这个A1模块的js不用写在旧的仓库里面,不受旧的技术选型的制约。

重点来了: 发布订阅模式(观察者模式)

观察者设计模式定义了对象间的一种一对多的依赖关系,以便一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动刷新。观察者模式-百度百科
具体操作如下:
比如我们在A模块操作之后需要A1模块来处理则只需要在A模块里触发一个自定义事件A1,然后把相关数据带过去,在A1模块里监听这个事件,做相应处理。示例代码如下:

// A模块
function A_active(){
    //balabala...做自己的事情
    $(document).trigger("A1",[data1,data2]);
}
//A1模块
 $(document).on("A1",function(data1,data2){
     //balabala,做自己的事情
 });

依次类推,你只需要在旧的代码里插入诸如

$(document).trigger("A1",[data1,data2]);

这样的代码,然后在新模块里监听对应的事件这样两个模块就解耦了。

发布-订阅模式弊端

世界上本没有什么救世主,也没有什么银弹。。。发布-订阅模式并不是万能的,这只是我解决实际项目的一点心得和记录,发布-订阅模式弊端也是有的

 发布者只能发布事件,并不知道订阅者有哪些,常年月累,订阅方可能遍布系统的各个角落。
             ---你终于变成了当初最讨厌的那个人--By 高德纳-尼古拉斯

解决这个问题:**只能收敛发布的事件,并且尽量减少订阅方,最主要的:文档,一定要在文档里记录哪些地方有订阅这些事件,这个文档可以是注释,也可以是完整的项目文档。
----未完待续--
https://www.noway.pub/p/101.html

查看原文

赞 2 收藏 0 评论 0

ccfnever 关注了用户 · 2019-10-14

青叶 @qingye2016

关注 27

ccfnever 赞了文章 · 2019-10-14

[tcp] WEB服务,Linux下的内核参数调优

前言:
web类应用一般会部署像nginx、tomcat、php等应用程序,使用默认的内核参数设置满足大部分场景,如果优化内核参数,也可以释放不少服务器性能,尤其是在高并发下

一.SYN状态的内核参数调优

大量SYN_SENT
这种是主动连接服务端,而未得到响应,也就是SYN超时,一般是服务端根本不存在或者无法访问
如,我随便telnet一个位置的IP和端口

telnet 172.18.11.110:90
[root@test bbs]# ss -an|grep SYN
SYN-SENT   0      1            172.16.196.145:55052        172.18.11.110:90

除了以上,还有种就是你的服务出现异常,比如mysql服务器宕机了,web服务去访问mysql数据库的时候就连不上,也会出现SYN_SENT状态,但无论哪种,都是主动发起连接导致的,因此业务上解决更好

net.ipv4.tcp_syn_retries = 2
新建连接如果无响应,内核要发送多少次SYN连接才放弃,默认值为5

在Linux下,默认重试次数为5次,该值不能大于255,重试的间隔时间从1s开始每次都翻倍(因为隔一秒重试后还会等待响应,因此实际上是从3秒开始),5次的重试时间间隔为3s, 7s, 15s, 31s, 63s,总共63s,TCP才会把断开这个连接。统计成公式2^(n+1) - 1,因此设置越大,翻倍越多,对应内网环境,这个值修改为2比较合适

大量SYN_RECV
大量的SYN出现有两种情况,可能是攻击,也可能是正常的业务请求,无论哪种,都大量的占用了服务器资源

net.ipv4.tcp_synack_retries = 2
跟参数net.ipv4.tcp_syn_retries一样,只是这个内核参数是控制回应SYN失败的重试次数,默认值也是5,和上面一样修改为2

其他内核参数调整
net.ipv4.tcp_syncookies = 1
开启SYN cookies,当出现SYN等待队列溢出时,启动cookies来处理

什么是SYN cookies?我们知道SYN攻击是一系列伪造IP源地址的SYN包,IP地址是随意选择且不提供攻击者任何的线索,SYN攻击持续直到服务的SYN队列被用满。如果启用该参数,此时SYN cookies会将TCP请求的SYN缓存起来,当服务器正常的时候,再处理,但是如果攻击并发很高很大,其实用处不大,因此只能少量防范

SYN cookies可参考:https://blog.csdn.net/chenmo1...

net.ipv4.tcp_max_syn_backlog = 65535
指定所能接受SYN同步包的最大客户端数量,即半连接上限,默认值为128,对于web服务,频繁大量的SYN同步包,应该放大这个值

注:这个值应该>=net.core.somaxconn,net.core.somaxconn后面会提到

二.FIN_WAIT_2状态的内核参数调优

FIN_WAIT_2是主动关闭端等待对端关闭连接的状态,如果被动关闭不发送FIN关闭连接,那么这个状态就会一直存在,当然Linux有针对该状态的超时时间,默认为60秒

net.ipv4.tcp_fin_timeout = 10

三.TIME_WAIT状态的内核参数调优

TIME_WAIT是主动关闭端的状态,也称为2MSL等待状态,也就是2倍的MSL时间。在RFC 793[Postel 1981c]指出MSL为2分钟,然而现实中的常用值是30秒,1分钟或者2分钟(Linux设置为30秒),Linux也没有提供能够修改TIME_WAIT状态时间的接口,除非重新编译系统内核

MSL的理解
MSL是英文Maximum Segment Lifetime的缩写,翻译为"最长报文段寿命",每个具体TCP实现必须选择一个报文段最大生存时间(Maximum Segment Lifetime),而这个最大生存时间是任何报文段被丢弃前在网络内的最长时间

MSL的时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL(time to live)字段,TTL可译为生存时间,IP数据报每经过一个路由器,它的值就减1,当这个值为0时,数据报则被丢弃

为什么等待2MSL
1.确保有足够的时间让服务端收到ACK,如没有收到,则会响应对方新的FIN+ACK封包。比如主动关闭端(客户端)发送了最后一个ACK报文段给被动关闭端(服务端),但这个ACK报文段有可能丢失,如果服务端没有收到这个ACK,那么处于LAST_ACK的服务端在超时后回重发FIN+ACK报文段,这样客户端就能在2MSL时间内收到这个重发的FIN+ACK报文段。如果客户端发送了最后的ACK报文不进入TIME_WAIT而是立即释放连接,那么就无法收到客户端重发的FIN+ACK报文段。因此等待2MSL是为了更安全的断开连接

2.有足够的时间让处于TIME_WAIT状态的连接不会跟后面的连接混在一起。比如一些延迟的包发过来,但是如果没有TIME_WAIT,那么就发到了新连接上,这样就混为一团,而如果是TIME_WAIT,则会丢弃这些延迟的包

等待2MSL的缺点
TCP连接在2MSL等待期间,这个处于TIME_WAIT状态的连接(客户端的IP地址和端口编号,服务器的IP地址和端口号)不能再被使用,它只能在2MSL结束后才能再被使用,而这些TIME_WAIT状态占用大量服务资源,对于web服务来说是不合理的

修改内核参数防止因为2MSL导致TIME_WAIT过多
对于web服务器,由于我们需要经常去连接mysql、redis或者一些RPC调用等,会有大量的主动关闭状态(TIME_WAIT),因此可以修改内核参数限制TIME_WAIT的数量

net.ipv4.tcp_max_tw_buckets = 20000
限制timewait 的数量,防止大量timewait导致系统负载升高,一旦达到限定值,则强制清理TIME_WAIT状态的连接并在打印系统日志(time wait bucket table overflow),该参数官方文档说明主要用来对抗DDos攻击

net.ipv4.tcp_tw_recycle= 1
启用timewait快速回收

net.ipv4.tcp_timestamps = 0
时间戳,0关闭,1开启。不能和net.ipv4.tcp_tw_recycle参数同时开启,因为一旦开启net.ipv4.tcp_tw_recycle,服务器就会检查包的时间戳,如果对方发来的包的时间戳是乱跳或者说时间戳是滞后的,这样服务器就不会回复,服务器会把带了"倒退"的时间戳包当作是"recycle"的tw连接的重传数据,不是新的请求,于是丢掉不回包,就容易出现syn不响应

net.ipv4.tcp_tw_reuse = 1
开启重用,允许将TIME-WAIT sockets 重新用于新的TCP 连接

TIME_WAIT总结
其实TIME_WAIT是主动断开连接,所以如果让对方主动断开连接的话,那么这个TIME_WAIT问题就对方的了。所以如果这个问题出现过多,多从业务着手,比如HTTP服务,NGINX设置keepalive参数(浏览器会重用一个TCP连接来处理多个HTTP请求),然后让客户端断开连接,当然这个要设置好keepalive_timeout的超时时间,因为有些浏览器可能不会主动断开连接

而如果是主动连接mysql、redis等后端调用,可以考虑使用长连接来避免TIME_WAIT过多的问题

四.长连接(keepalive)的内核参数调整

Linux下,keepalive不是默认开启,也无内核参数控制,它需要在TCP的socket中单独开启,Linux内核影响keepalive的参数目的仅仅是探测TCP连接是否存活,然后处理异常连接

net.ipv4.tcp_keepalive_time = 120 单位秒,表示TCP连接在多少秒没有数据报文传输时启动探测报文,探测连接是否正常
net.ipv4.tcp_keepalive_intvl = 5 单位秒,前后探测报文之间的时间间隔
net.ipv4.tcp_keepalive_probes = 3 探测次数,超过设置后丢弃

五.TCP/UDP内存参数调整

(1)TCP内存使用设置

针对TCP socket buffer
net.ipv4.tcp_mem = 94500000 915000000 927000000
指定TCP内存的整体使用状况,单位为页。这3个值为TCP整体内存【低、压力、高】,在web服务中,放大这个值即可
第一个值tcp_mem[0]:当TCP全局分配的页数低于此数时,TCP不调整其内存分配
第二个值tcp_mem[1]:当TCP分配的内存量超过这个页数,进入内存压力模式,TCP调节内存消耗
第三个值tcp_mem[2]:TCP全局使用的最大页数分配,这个会值覆盖任何其他限制,如超过,所有的新的TCP的buffer(缓冲区)内存分配都会失败

其实我们可以设置这个值较大,只要不限制系统分配内存,然后以监控来应对内存问题,一般来说,根据业务所选配置,很难将内存耗尽,否则优化的就不仅仅是这个参数了

net.ipv4.tcp_rmem = 4096 87380 6291456
net.ipv4.tcp_wmem = 4096 16384 4194304
上面两组参数表示单个TCP连接上的读写buffer(缓冲)内存上限,单位字节,这三个值分别为最小值、默认值(会覆盖rmem_default、wmem_default配置)、最大值

最小值:TCP socket的发送缓冲区(tcp_rmem)/接收缓冲区(tcp_wmem)的内存,默认1页(4K)

默认值:TCP socket使用的发送缓冲区(tcp_rmem)/接收缓冲区(tcp_wmem)初始大小,这个值会覆盖(net.core.wmem_default/net.core.rmem_default),一般设置要低于(net.core.wmem_default/net.core.rmem_default)这个值,默认值为16K

最大值:TCP socket使用的发送缓冲区(tcp_rmem)/接收缓冲区(tcp_wmem)的最大大小,这个值不会覆盖(net.core.wmem_max/net.core.rmem_max),默认为4M

这两个内核参数的设置主要是针对每一个TCP连接来说的,使用默认设置就差不多了,如果设置太大,单个TCP连接占用过多内存也是有问题的

什么是TCP读写buffer(缓冲)?
实际上,TCP连接所用内存的多少是由读写buffer大小决定,对读buffer来讲,当收到对端连接的TCP报文时,会导致读buffer内存增加,如果这个报文加上当前读buffer内存超过tcp_rmem[3]上限,那么该报文将被丢弃。只有当调用read、recv这样的方法读取TCP流时,读buffer内存就会减少,因此读buffer内存是一个动态变化的,用多少就分配多少buffer,如果这个连接空闲时,而用户进程已经把连接上收到的数据都消费了,那么读buffer使用的内存就为0了

对于写buffer也是一样的,在socket编程中,当调用send或者write时,就会造成写buffer增大,那么什么时候减少?就是当接收到对端TCP连接发来的ACK确认了报文成功发送时,写buffer就会减少,类似于我给你发一个文件,我先拷贝出来发给你,我确认你收到了,我就把这个源文件删除,以免占用空间,如果确认没收到,那么我会重发

所以读写buffer是一直不停变化的,那么怎样的场景会导致读写buffer达到上限呢?就读buffer而言,比如接收TCP对端报文,对端发了很多很多报文,我读取后无法及时读取(read和recv),导致读buffer堆积越来越多,最终达到上限,最后丢弃报文,写buffer也一样,send或者write大量的报文时,如果TCP对端不能及时read和recv就会导致写buffer堆积。

针对系统的读写buffer参数调整
net.core.rmem_default = 4194304 默认读buffer大小,单位字节
net.core.wmem_default = 4194304 默认写buffer大小,单位字节
net.core.rmem_max = 4194304 最大读buffer大小,单位字节
net.core.wmem_max = 4194304 最大写buffer大小,单位字节
看到其定义,是不是觉得跟net.ipv4.tcp_mem、net.ipv4.tcp_rmem、net.ipv4.tcp_wmem含义很重合呢?

其实(net.ipv4.tcp_mem、net.ipv4.tcp_rmem、net.ipv4.tcp_wmem)这几个参数只控制TCP socket的内存大小,而且如果遇到TCP socket申请内存,(net.core.rmem_default、net.core.wmem_default)会被(net.ipv4.tcp_rmem、net.ipv4.tcp_wmem)覆盖

所以(net.core.rmem_default、net.core.wmem_default、net.core.rmem_max、net.core.wmem_max)控制系统所有协议的读写buffer大小

(2)UDP协议内存使用设置

net.ipv4.udp_mem = 752832 1003776 1505664
net.ipv4.udp_rmem_min = 4096
net.ipv4.udp_wmem_min = 4096
这几个参数针对UDP协议,则跟上面TCP的含义一致

六.其他内核参数

net.ipv4.ip_local_port_range = 1024 65000
表示用于向外连接的临时端口范围。缺省情况下很小:32768到61000,因为主动连接需要用到很多临时端口(如连接mysql、redis),而临时端口最大值为(2^16-1)65535,1000之前一般为系统保留端口,所以建议设置为1024到65000的较大范围

net.core.somaxconn = 65535
net.core.somaxconn表示socket监听(listen)的backlog上限,backlog是socket的监听队列,也就是服务端所能accept(socket编程中accpet()函数为建立TCP连接接受连接状态)即处理数据的最大客户端数量队列,默认值为128,如果队列满了的时候新来一条建立连接,该连接会被拒绝

该值应当小于等于net.ipv4.tcp_max_syn_backlog,因为net.ipv4.tcp_max_syn_backlog参数控制的SYN队列客户端的数量,还在建立连接之前,因此设置为65535一样比较合适

fs.file-max = 6553600
设置系统所有进程一共可以打开多少个文件句柄,这是一个系统级的设置,管控的是所有进程总共可以同时打开多少文件句柄,如果多个进程打开了较多文件就会导致文件句柄不足,因此设置较大值,不过要注意程序打开的文件越多,就占用更多的内存,因此要根据业务和服务器配置起来设置

如果想单独对某个进程设置可以打开多少文件句柄,那么可以使用ulimit -n命令设置,但该命令只对当前session生效,默认值为1024
ulimit -n 655350

也可以写入文件永久生效,对每个进程的打开文件数量限制
vim /etc/security/limits.conf
* soft nofile 655350
* hard nofile 655350

总结

现在多数线上业务,服务器很少暴露在外网了,前端一般有负载均衡、防火墙等代理。甚至服务器已经变成VPC(虚拟内网)环境,将这些服务器隔离在外网环境之外,这样就减少了像DDOS等攻击,这些攻击一般都让外部代理承受了。

对于服务器的一些内核性能参数范围,如果网络环境及架构设计好,一些范围参数可以设置的偏大,性能偏极限一些,这样能最大释放服务器的性能,其他的就用系统默认的参数配置即可。对于WEB服务的优化,是多方面的,内核参数仅仅是释放了服务器本该有的性能,而更高的承载能力,需要从服务器配置、网络、架构、数据库及缓存和实际业务应用等多方面着手,不同的调整满足不同的需求

RT:以上有些地方可能会解释得不对,还望指正,一起学习

查看原文

赞 3 收藏 2 评论 0

ccfnever 赞了文章 · 2019-10-14

linux awk命令

前言:
最近复习了下awk,记录方便下次查阅

echo '11 22' | awk '{print $1}'
echo |awk '{print "hello world!"}'

一.awk变量

内置常用变量

$0      当前记录,当前行所有列
$1~$n   当前记录的第n个字段,字段间由FS分隔
FS      输入域分隔符,等价于命令行-F,默认为空格
NF      当前记录中的字段个数,就是有多少列,列总数,$NF则表示最后一列
NR      已经读取的记录数,就是第几行,从1开始
FNR     当前记录数
RS    控制记录分隔符,默认为换行符
OFS   输出字段分隔符 默认也是空格
ARGC  命令行参数个数
ARGV  命令行参数排列
FILENAME awk浏览的文件名

自定义变量类型

echo |awk 'i="hello world"{print i}'
echo |awk 'i=1122 {print i}'

数组

echo |awk '{a[1]="hello";a[2]="world!";print a[1],a[2]}'

二.awk常用逻辑运算

?           条件表达操作符
|| && !     并、与、非
~  !~       匹配操作符,包括匹配 不匹配
\+ - * / % ^ 算术操作符
++ --        前缀和后缀
= += *= / = %= ^ =  赋值操作符 
< <= == != >= >     关系操作符     
BEGIN  在输出界面第一行输出相关
END    在输出界面最后一行输出相关

三.awk条件判断

1.直接在最外层

echo '11 22' |awk '$1==11{print $2}'

2.使用if语句

```echo '11 22' |awk '{if($1==11) print $2}'```

3.与或非

与
echo '11 22' |awk '{if(($1==11) && ($2==22))  print $2}'
或
echo '11 22' |awk '{if(($1==10) || ($2==22))  print $2}'
非
echo '11 22' |awk '{if($1!=10)  print $2}'

4.匹配

 ~      模糊匹配
 ==     精确匹配
 !~     不匹配
echo 'ansible new switf' |awk '{if($1 ~ i)  print $1}'
echo 'ansible new switf' |awk '{if($1 !~ 0)  print $1}'

5.正则匹配

last |awk '/root/{print $0}'
last |awk '/roo*/{print $0}'
last |awk '/^root /{print $0}'

四.awk循环

for(i=1;i<=10;i++)
类似C等语言的循环
使用awk将每行插入一个符号"|"和tab建

last |awk '{for(i=1;i<=NF;i++){printf $i "|\t"} print ""}'

NF 每一行所拥有的总字段数

for(i in 数组)
类似shell

echo|awk 'BEGIN{a[1]=1;a[2]=2}END{for(i in a) print i,a[i]}'

五.常用内置函数

gsub(r,s)          在整个$0中用s替代r,相当于 sed 's///g'
gsub(r,s,t)        在整个t中用s替代r
index(s,t)         返回s中字符串t的第一位置
length(s)          返回s长度
match(s,r)         测试s是否包含匹配r的字符串
split(s,a,fs)      在fs上将s分成序列a 
sprint(fmt,exp)    返回经fmt格式化后的exp
sub(r,s)           用$0中最左边最长的子串代替s,相当于 sed 's///'
substr(s,p)        返回字符串s中从p开始的后缀部分
substr(s,p,n)      返回字符串s中从p开始长度为n的后缀部分

六.运维常用的awk

1.打印指定倒数字段数
利用NF打印倒数字段,比如打印倒数第2行

last |awk '{print $(NF-1)}'

2.打印文件第1000行到2000行

awk '1000<=FNR && FNR <=2000' file
awk '{if(1000<=FNR && FNR<=2000) print $0}' file

3.使用for循环、数组、自加对netstat的tcp状态统计

netstat -an |awk '/^tcp/ BEGIN{s[$NF]++} END{for(a in s) print a,s[a]}'

4.替换

eg: test.txt

zhangsan 70 99 88 77 good
lisi 90 77 66 88 good
sansan 80 88 78 89 good
awk '{if($2>=90) gsub($NF,"Very Good")} {print $0}' test.txt
zhangsan 70 99 88 77 good
lisi 90 77 66 88 Very Good
sansan 80 88 78 89 good
查看原文

赞 2 收藏 0 评论 0

认证与成就

  • 获得 95 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-05-20
个人主页被 1.3k 人浏览