执鸢者

执鸢者 查看完整档案

衡水编辑华中科技大学  |  机械科学与工程学院 编辑  |  填写所在公司/组织填写个人主网站
编辑

摸摸头,编程使我快乐。
公众号:执鸢者
欢迎关注公众号,并加群,一起加油

个人动态

执鸢者 发布了文章 · 1月17日

前端也要懂机器学习(下)

关注公众号“执鸢者”,回复“资料”获取500G资料(各“兵种”均有),还有专业交流群等你一起来潇洒。(哈哈)

上一篇文章讲述了机器学习的基本知识点,这一篇就开启一些算法的摸索之路。既然我们是前端研发工程师,那就选择ml.js这个库进行编码。本次涉及到的算法包含:KNN、决策树、随机森林、朴素贝叶斯、支持向量机、线性回归、K-均值聚类算法,这七个算法横跨监督学习算法(分类算法、回归算法)、非监督学习算法,可以作为前端入门机器学习的必修课程,也可作为既将到来的端智能时代的必读刊物。

一、监督学习算法

1.1 分类算法

1.1.1 K-近邻分类算法(KNN)

一、定义

如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。(通常k是不大于20的整数)

二、优缺点

  1. 优点

    • 简单有效
    • 精度高
    • 对异常值不敏感
    • 无须训练
  2. 缺点

    • 计算复杂度高、空间复杂度高(计算量大,内存开销大)
    • 必须指定K值,K值选择不当则分类精度不能保证。(K值取很小,容易受到异常点的影响,容易出现过拟合;K值取很大,受到样本均衡问题)

三、计算距离

对于KNN算法,最核心的内容是计算距离,两个样本之间的距离可以通过欧氏距离计算。其计算公式为:

四、应用场景

小数据场景(几千~几万样本),可用于字符识别、文本分类、图像识别等领域

五、代码

const KNN = require('ml-knn');

// 训练集特征
const dataset = [
    [1.0, 1.1],
    [1.0, 1.0],
    [0, 0],
    [0, 0.1]
];

// 训练集目标
const labels = ['A', 'A', 'B', 'B'];

// 实例化KNN算法(进行训练)
const knn = new KNN(dataset, labels, { k: 3});

// 需要进行预测的点
const predictDataSet = [[0, 0], [3, 1]];

// 进行预测
predictLabels = knn.predict(predictDataSet);
console.log(predictLabels);// [ 'B', 'A' ]

1.1.2 决策树

一、定义

决策树(Decision Tree)是在已知各种情况发生概率的基础上,通过构成决策树来求取净现值的期望值大于等于零的概率,评价项目风险,判断其可行性的决策分析方法,是直观运用概率分析的一种图解法。决策树是一种树形结构,其中每个内部节点表示一个属性上的测试,每个分支代表一个测试输出,每个叶节点代表一种类别。

二、优缺点

  1. 优点

    • 计算复杂度不高
    • 输出结果易于理解
    • 对中间值的缺失不敏感
    • 可以处理连续和种类字段
    • 可以处理不相关特征数据
  2. 缺点

    • 可能出现过拟合现象,需要进行剪枝操作
    • 对于各类别样本数量不一致的数据,信息增益偏向于那些更多数值的特征

三、应用场景

常用于解决分类和回归问题,用该算法的前提条件是:
1. 具有决策者期望达到的明确目标
2. 存在决策者可以选择的两个以上的可行的备选方案
3. 存在决策者无法控制的两个以上不确定因素
4. 不同方案在不同因素下的收益或损失可以计算出来
5. 决策者可以估计不确定因素发生的概率

四、重点知识点

  1. 信息熵
信息是很抽象的概念,很难进行量化对量,为了解决对信息的量化度量问题,香农提出了“信息熵”的概念。信息熵是在信息的基础上,将有可能产生的信息定义为一个随机变量,变量的期望就是信息熵。信息熵的计算公式为(单位为比特):

注:熵是用来度量不确定性,熵越大则其不确定性越大,反之越小

  1. 信息增益
信息增益在决策树算法中是用来选择特征的指标,信息增益越大,则这个特征的选择性越好,在概率论中定义为:待分类的集合的熵和选定某个特征的条件熵之差。其计算公式为:

注:

  • g(D, A)表示特征A对训练数据集D的信息增益
  • H(D)表示集合D的信息熵
  • H(D|A)表示条件熵
  1. 常用算法

(1)ID3算法

ID3算法是采用信息增益作为特征选择的标准,信息增益越大,说明按此特征分类后越能消除信息的不确定性。

(2)C4.5算法

ID3算法具有两大缺点:一个是类别越多的特征计算出的信息增益越大,易导致生成的决策树广而浅;另一个是只能处理离散变量,不能处理连续变量。C4.5是在ID3的算法基础上采用信息增益率作为特征选择,通过增加类别的惩罚因子,规避了类别越多信息增益越大的问题,同时也可以对连续变量通过均值离散化的方式解决无法处理连续变量的问题。

(3)CART算法

C4.5存在不能处理回归问题的缺点,该缺点由CART解决。CART不在通过信息熵的方式选取最优划分特征,而是采用基尼系数(基尼不纯度),两者衡量信息量的作用相当,但基尼系数由于没有对数运算,可大大减少计算开销。

五、代码

const irisDataset = require('ml-dataset-iris');
const { DecisionTreeClassifier } = require('ml-cart');

// 获取训练集中的特征值
const dataSet = irisDataset.getNumbers();
// 获取训练集中的目标值,并转换为标量
const labels = irisDataset
    .getClasses()
    .map(elem => irisDataset.getDistinctClasses().indexOf(elem));

// 实例化决策树的分类器
const dTClassifier = new DecisionTreeClassifier({
    gainFunction: 'gini',
    maxDepth: 10,
    minNumSamples: 3
});

// 进行训练
dTClassifier.train(dataSet, labels);

const predictDataset = [[5.1,3.5,1.4,0.2]];

// 进行预测
const result = dTClassifier.predict(predictDataset);

// 结果转换为对应的文本形式
console.log(result.map(value => irisDataset.getDistinctClasses()[value]));

1.1.3 随机森林

一、定义

在机器学习中,随机森林是一个包含多个决策树的分类器,其输出的类别是由个别树输出的类别的众数而定(随机森林就是通过集成学习的思想将多棵树集成的一种算法,其基本单元是决策树)。

二、优缺点

  1. 优点

    • 具有极好的准确率(由集成算法的特点引入)
    • 抗过拟合能力:通过平均决策树,降低过拟合的风险性(由随机这个特点引入)
    • 能够有效地运行在大数据集上,处理具有高维特征的输入样本,而且不需要降维
    • 能够评估各个特征在分类问题上的重要性
  2. 缺点

    • 在某些噪音较大的分类或回归问题上会过拟合
    • 比决策树算法更复杂,计算成本更高

三、重要知识点

  1. 随机森林的每棵树的生成规则(A表示训练集总样本个数、N表示训练样本个数、M表示特征个数)

(1)对于每棵树随机有放回的从训练集中抽取N个训练样本,作为该树的训练集
(2)指定一个常数m<<M,随机地从M个特征中选取m个特征子集,每次树进行分裂时,从这m个特征中选择最优的
(3)每棵树都尽最大程度的生长,并且没有剪枝过程。

  1. 为什么要随机抽样训练集?

随机抽样是为了保证每棵树的训练集都不一样,若不随机抽样会导致最终训练出的分类结果完全一样。

  1. 为什么要有放回的抽样?

有放回的抽样才能保证每次抽取时的概率是一样的,达到独立同分布,可保证每一棵决策树都是相互独立的。

  1. 随机森林分类效果(错误率)相关因素?

(1)森林中任意两棵树的相关性:相关性越大,错误率越大

(2)森林中每棵树的分类能力:每棵树的分类能力越强,整个森林的错误率越低

(注:减小特征选择个数m,树的相关性和分类能力会相应降低,反之会随之增大)

四、代码

const irisDataset = require('ml-dataset-iris');
const { RandomForestClassifier } = require('ml-random-forest');

// 获取训练集中的特征值
const dataSet = irisDataset.getNumbers();
// 获取训练集中的目标值,并转换为标量
const labels = irisDataset
    .getClasses()
    .map(elem => irisDataset.getDistinctClasses().indexOf(elem));

// 实例化分类器
const rFClassifier = new RandomForestClassifier({
    seed: 3,
    maxFeatures: 0.8,
    replacement: true,
    nEstimators: 25
});

// 进行训练
rFClassifier.train(dataSet, labels);

const predictDataset = [[5.1,3.5,1.4,0.2]];

// 进行预测
const result = rFClassifier.predict(predictDataset);

// 结果转换为对应的文本形式
console.log(result.map(value => irisDataset.getDistinctClasses()[value]));

1.1.4 朴素贝叶斯

一、定义

朴素贝叶斯法(NBC)是基于贝叶斯定理与特征条件独立假设的分类方法。先通过已给定的训练集,以特征词之间独立作为前提假设,学习从输入到输出的联合概率分布,再基于学习到的模型,输入X求出使得后验概率最大的输出Y。

二、优缺点

  1. 优点

    • 朴素贝叶斯模型发源于古典数学理论,有稳定的分类效率
    • 对缺失数据不敏感,算法简单,常用于文本分类
    • 分类准确度高,速度快
  2. 缺点

    • 使用了样本独立性的假设,如果特征属性由关联性其效果较差

三、应用场景

  1. 文本分类
  2. 文字识别
  3. 图像识别

四、重要知识点

  1. 贝叶斯公式

注:贝叶斯公式是打通P(W|C)和P(C|W)的桥梁

  1. 为什么引入拉普拉斯平滑系数

为了防止计算出的分类概率为0,所以引入拉普拉斯平滑系数,即让P(W1|C)不为0,其计算公式为:

注:其中α为指定系数,一般为1;m为训练文档中统计出的特征词个数

  1. 三种贝叶斯模型

(1)高斯分布朴素贝叶斯——用于一般分类问题

(2)多项式分布朴素贝叶斯——适用于文本数据(特征表示的是次数)

(3)伯努利分布朴素贝叶斯——适用于伯努利分布、文本数据(特征表示的是是否出现)

五、代码

const irisDataset = require('ml-dataset-iris');
const { GaussianNB } = require('ml-naivebayes');

// 获取训练集中的特征值
const dataSet = irisDataset.getNumbers();

// 获取训练集中的目标值
const labels = irisDataset
    .getClasses()
    .map(elem => irisDataset.getDistinctClasses().indexOf(elem));


//实例化分类器
const gaussianNB = new GaussianNB();
// 进行训练
gaussianNB.train(dataSet, labels);

const predictDataset = [[5.1,3.5,1.4,0.2]];

// 进行预测
const result = gaussianNB.predict(predictDataset);

// 结果转换为对应的文本形式
console.log(result.map(value => irisDataset.getDistinctClasses()[value]));

1.1.5 支持向量机

一、定义

支持向量机(SVM)是一类按监督学习方式对数据进行二元分类的广义线性分类器,其决策边界是对学习样本求解的最大边距超平面。

二、优缺点

  1. 优点

    • 有严格的数学理论支持,可解释性强,不依靠统计方法,简化了通常的分类和回归问题。
    • 上述支持向量决定了最终结果,对异常值不敏感,可以帮助抓住关键样本,剔除大量冗余样本
    • 该算法简单且具有较好的“鲁棒性”
    • 计算的复杂度取决于支持向量的数目,而不是样本空间的维数,某种意义上避免了“维数灾难”
    • 泛化能力较强
  2. 缺点

    • 对大规模训练样本难以实施:SVM的空间消耗主要是存储训练样本和核矩阵,由于SVM是借助二次规划来求解支持向量,而求解二次规划将涉及m阶矩阵的运算,当m数目很大时该矩阵的存储和计算将耗费大量的机器内存和运算时间。
    • 解决多分类问题困难:经典的支持向量机算法只给出了二类分类算法,解决多分类问题需要通过多个二类支持向量机的组合来解决(一对多组合模式、一对一组合模式和SVM决策树)。
    • 对参数和核函数选择敏感:支持向量机性能的优劣主要取决于核函数的选取。

三、应用场景

SVM在各领域的模式识别问题中有应用,包括人像识别、文本分类、手写字符识别、生物信息学等。

四、重要知识点

  1. 重要概念

(1) 线性可分:在二维空间上,两类点被一条直线完全分开叫做线性可分

(2)最大间隔超平面:从二维扩展到多维空间中,将两个点集完全正确地划分开就形成一个超平面,为了使这个超平面更具鲁棒性,则会找最佳超平面(即为最大间隔超平面,该平面是以最大间隔把两类样本分开的超平面)。(两类样本分别分割在该超平面的两侧、两侧距离超平面最近的样本点到超平面的距离被最大化了)

(3)支持向量:样本中距离超平面最近的一些点叫支持向量

(4)软间隔:对于不能够完全线性可分的样本可引入软间隔,相比于硬间隔的苛刻条件,软间隔允许个别样本出现在间隔带里面。(注:硬间隔和软间隔均是在说样本的完全线性可分或者大部分样本点的线性可分)

  1. 对于线性不可分处理方式

将线性不可分样本映射到高维空间中,使样本在高维空间线性可分,此时由于维度提高会导致计算量增大,所以需要使用核函数帮助处理,引入核函数后就不需要计算高维甚至无穷维空间的内积了。

  1. 引入核函数的好处

(1)减少了计算量

(2)减少了存储数据的内存使用量

  1. 常见核函数分类

(1)线性核函数
(2)多项式核函数
(3)高斯核函数

五、代码

const SVM = require('libsvm-js/asm');

const svm = new SVM({
    kernel: SVM.KERNEL_TYPES.RBF,
    type: SVM.SVM_TYPES.C_SVC,
    gamma: 1,
    cost: 1
});

const dataSet = [[0, 0], [1, 1], [1, 0], [0, 1]];
const labels = [0, 0, 1, 1];
// 进行训练
svm.train(dataSet, labels);

// 进行预测
const predictedLabel = svm.pred

1.2 回归算法

1.2.1 线性回归

一、定义

线性回归是利用回归方程(函数)对一个或多个自变量(特征值)和因变量(目标值)之间关系进行建模的一种分析方式。只有一个自变量的情况称为单变量回归,大于一个自变量的情况叫做多元回归。

二、优缺点

  1. 优点

    • 思想简单、容易实现、建模迅速,对于小数据量、简单关系情形的很有效
    • 是需要强大的非线性模型的基础
    • 理解容易,其结果具有很好的可解释型,有利于决策分析。
    • 能解决回归问题
  2. 缺点

    • 对于非线性数据或者数据特征间具有相关性多项式回归难以建模
    • 难以很好地表达高度复杂的数据

三、应用场景

  1. 趋势线(时间序列数据的长期走势)
  2. 流行病学
  3. 金融(分析和计算投资的系统风险)
  4. 经济学(预测消费支出、固定投资支出等)

四、重要知识点

  1. 回归目的——预测数值型的目标值
  2. 回归性能评价指标——均方误差(MSE)
  3. 过拟合

(1)定义:一个建设在训练数据上能够获得比其它假设更好的拟合,但是在测试数据集上却不能很好地拟合数据,此时认为这个假设出现了过拟合现象。(模型过于复杂)

(2)原因:原始特征过多,存在一些嘈杂特征

(3)解决办法:正则化

4.欠拟合

(1)定义:一个假设在训练数据集上不能获得很好的拟合,并且在测试数据集上也不能很好地拟合数据,此时认为这个假设出现了欠拟合的现象。(模型过于简单)

(2)原因:学习到数据的特征过少

(3)解决办法:增加数据的特征数量

  1. 正则化

(1)L2正则化

L2正则化可以使得其中一些W都很小(接近于0),削弱某个特征影响。Ridge回归就是用的L2正则化。

(2)L1正则化

L1正则化可以使得其中一些W的值直接为0,删除整个特征的影响。LASSO回归用的就是L1正则化。

五、代码

const SimpleLinearRegression = require('ml-regression-simple-linear');

const x = [0.5, 1, 1.5, 2, 2.5];
const y = [0, 1, 2, 3, 4];

const regression = new SimpleLinearRegression(x, y);

const result = regression.predict(3);

console.log(result);

二、非监督学习算法

2.1 K-均值聚类算法

一、定义

K均值聚类算法是一种迭代求解的聚类分析算法,其步骤为:
  1. 随机设置K个特征空间内的点作为初始的聚类中心
  2. 对于其它每个点计算到K个中心的距离,位置的点选择最近的一个聚类中心点作为标记类别
  3. 接着重新计算出每个聚类的新中心点(平均值)
  4. 如果计算得出的新中心点与原中心点一样,则结束,否则重新进行第二步过程

二、优缺点

  1. 优点

    • 原理简单,容易实现,算法复杂度低
    • 可解释度较强
    • 处理大数据集的时候,该算法可以保证较好的伸缩性
    • 当簇近似高斯分布的时候效果很好
  2. 缺点

    • K值需要人为设定,不同K值得到的结果不一样
    • 对初始的簇中心敏感,不同选取方式会得到不同结果
    • 对异常值敏感
    • 需样本存在均值(限定数据种类)
    • 不适合太离散的分类、样本类别不均衡的分类、非凸形状的分类
    • 可能收敛到局部最小值

三、代码

const kmeans = require('ml-kmeans');

// 需要进行聚类的全部数据
const data = [[1, 1, 1], [-1, -1, -1], [-1, -1, -1.5], [1, 2, 1]];

// 质心
const centers = [[1, 2, 1], [-1, -1, -1]];

const ans = kmeans(data, 2, {
    initialization: centers
});

console.log(ans);

相关文章

前端也要懂机器学习(上)

参考文献

支持向量机 SVM(非常详细)

1.如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到

2.关注公众号执鸢者,领取学习资料(前端“多兵种”资料),定期为你推送原创深度好文

查看原文

赞 0 收藏 0 评论 0

执鸢者 发布了文章 · 1月10日

前端也要懂机器学习(上)

关注公众号“执鸢者”,回复“资料”获取500G资料(各“兵种”均有),还有专业交流群等你一起来潇洒。(哈哈

本文撰写背景:<br/>
近年来机器学习的热度不断升高,前端领域也在不断布局,从大的研究方向来看,有前端智能化、端智能;从框架这个角度来看,也有很多js版的框架,例如TensorFlow.js、Ml.js、Brain.js等。虽然当前来看其在前端落地的应用还不是很多,但是把握趋势就是把握未来。本文将通过上下两篇来对机器学习的内容进行阐述,这是第一篇,主要介绍一些基础知识和特征工程;后续推出的一批主要利用阐述机器学习中的监督学习算法和非监督学习算法。(注:该文章不涉及大量的数学公式推导,适合前端开发同学入门机器学习的开胃菜)

一、基础

1.1 定义

机器学习是从数据中自动分析获得模型,并利用模型对未知数据进行预测。

1.2 算法分类

机器学习的前提是数据,根据数据中是否有目标可以划分为:监督学习算法和无监督学习算法。
  • 监督学习算法——输入数据是由输入特征值和目标值所组成。
  • 无监督学习算法——输入数据是由输入特征值和目标值所组成

1.3 如何选择合适算法

机器学习有这么多算法,面对一个问题时应该如何选择合适的算法成为了不可避免的问题,下面就来叙述两条选择合适算法的黄金法则。
  1. 确定使用机器学习算法的目的。

    • 若想要预测目标变量的值——监督学习算法

      • 目标变量为离散型——分类算法
      • 目标变量为连续型——回归算法
    • 若无目标变量值——无监督学习

      • 将数据划分为离散的组是唯一需求——聚类算法
      • 除将数据划分为离散的组,还需要估计数据与每个组的相似度——密度估计算法
  2. 需要分析或收集的数据是什么,了解其数据特征

    • 特征值是离散型变量还是连续型变量
    • 特征值中是否存在缺失的值
    • 何种原因造成缺失值
    • 数据中是否存在异常值
    • 某个特征发生的频率如何
    • ……

1.4 整体流程

不管多么复杂的内容经过前人的总结最会有一套方法论供我们这样的小白使用,下面就来阐述一下机器学习通用的流程。
  1. 获取数据
获取数据是机器学习的第一步,例如使用公开的数据源、爬虫获取数据源、日志中获取数据、商业数据等。

2.数据处理

得到数据后并不一定符合使用需求,所以需要进行数据清洗、数据填充、数据格式转换,达到减小训练的数据量,加快算法的训练时间的目的。

3.特征工程

特征工程是使用专业背景知识和技巧处理数据,使得特征能在机器学习算法上发挥更好的作用的过程,直接影响机器学习的效果,主要包含特征提取、特征预处理、特征降维

4.算法训练

将处理之后的数据输入到合适的算法进行训练。

5.模型评估

使用测试集来评估模型的性能.模型性能指标有很多,比如在分类模型中常用的有错误率,精准率,召回率,F1指标,ROC等。

6.应用

将机器学习算法转换为应用程序执行实际任务,从而检验该算法是否可以在实际工作中正常使用。

二、特征工程

业界流传着一句话 “数据决定了机器学习的上限,而算法只是尽可能逼近这个上限”,这里的数据指的就是经过特征工程处理后的数据。特征工程是使用专业背景知识和技巧处理数据,使得特征能在机器学习算法上发挥更好的作用的过程,可见特征工程在机器学习中的重要地位。对于特征工程主要分为三步:特征抽取、特征预处理、特征降维。
注:特征工程是一个很深的学科,此处不展开阐述。

2.1 特征提取

特征提取指的就是将任意数据(如文本或图像)转换为可用于机器学习的数字特征,常用的主要包含:字典特征提取、文本特征提取、图像特征提取。

2.2 特征预处理

通过特征提取,能得到未经处理的特征,该特征具有以下问题:
  1. 量纲不同:特征可能具有量纲,导致其特征的规格不一样,容易影响(支配)目标结果,使得一些算法无法学习到其它的特征,需要进行无量纲化处理。
  2. 信息冗余:对于某些定量特征,其包含的有效信息为区间划分,需要进行二值化处理。
  3. 定性特征不能直接使用:某些机器学习算法和模型只接受定量特征的输入,则需要将定性特征转换为定量特征,可通过哑编码实现。

2.2.1 无量纲化

无量纲化使不同规格的数据转换到同一规则,常用方法有归一化和标准化
  1. 归一化

    • 定义<br/>
      对原始数据进行线性变换,使得结果映射到[0,1]之间。
    • 计算公式<br/>
    • 特点<br/>
      最大最小值容易受到异常点影响,稳定性较差。
  2. 标准化

    • 定义<br/>
      将原始数据进行变换到均值为0、标准差为1的范围内
    • 计算公式<br/>
    • 特点<br/>
      较少的异常点对结果影响不大,稳定性较好。

2.2.2 定量特征二值化

对于定量特征可进行二值化,通过选取一个合适的阈值,大于某阈值的为1,小余等于某阈值的为0。

2.2.3 定性特征哑编码

大部分算法对于定性特征无法训练模型,利用哑编码(one-hot)可以将定性特征处理为定量特征用于模型训练,其基本思想是:将离散特征的每一种值看成一种状态,若该特征有N个值,则存在N种状态,例如女孩子的头发按照辫子的多少可以划分为:无辫子、一个辫子、两个辫子、多个辫子,则该特征有4种状态,利用哑编码可以表示为:
  1. 无辫子 ====》[1, 0, 0, 0]
  2. 一个辫子 ====》[0, 1, 0, 0]
  3. 两个辫子 ====》[0, 0, 1, 0]
  4. 多个辫子 ====》[0, 0, 0, 1]

2.3 特征降维

在机器学习中对维度较高的数据进行处理时会极大消耗系统资源,甚至产生维度灾难。在某些限定条件下,通过降维的方式可降低随机变量的个数,用一个低维度向量来表示原始高维度的特征。常用的降维的方式有:特征选择、PCA、LDA等。

2.3.1 特征选择

数据预处理完毕之后需要选择有意义的特征进行训练,特征选择从以下两方面考虑:
  1. 特征是否发散:若某特征不发散(例如方差接近为0),则认为该特征无差异。
  2. 特征与目标的相关性:优先选择与目标相关性较高的特征。
常用的特征选择方式有:Filter(过滤法)、Wrapper(包装法)、Embedded(集成法)。
2.3.1.1 Filter(过滤法)
先进行特征选择,然后去训练学习器,其特征选择的过程与学习器无关。其主要思想是给每一维的特征赋予权重,权重代表该特征的重要性,然后设定阈值或者待选择阈值的个数选择特征。常用方法有方差选择法(低方差特征过滤)、相关系数法等。

一、低方差特征过滤

方差指的是各变量值与其均值离差平方的平均数,是测算数值型数据离散程度的重要方法,方差越大则表征数据的离散程度越大,反之越小。对于数据中的特征值,方差小则表示特征大多样本的值比较相近;方差大则表示特征很多样本的值都有差别。低方差特征过滤正是基于该思想,通过设定方差阈值来去掉小于该阈值的特征。方差计算公式如下:

二、相关系数法

皮尔逊相关系数(r)定义为两个变量之间的协方差和标准差的商,是反映变量之间相关关系密切程度的统计指标,其值r范围为[-1, 1],含义为:
  • 当r > 0时表示两变量正相关
  • r < 0时,两变量为负相关
  • 当|r|=1时,表示两变量为完全相关
  • 当r=0时,表示两变量间无相关关系
  • 当0<|r|<1时,表示两变量存在一定程度的相关。且|r|越接近1,两变量间线性关系越密切;|r|越接近于0,表示两变量的线性相关越弱

2.3.1.2 Wrapper(包装法)
把要使用的分类器作为特征选择的评价函数,对于特定的分类器选择最优的特征子集。其主要思想是将子集的选择看作是一个搜索寻优问题,生成不同的组合,对组合进行评价,再与其它的组合进行比较。常用方法有递归特征消除法。
  • 递归特征消除的主要思想是反复的构建模型(如SVM或者回归模型)然后选出最好的(或者最差的)的特征(可以根据系数来选),把选出来的特征选择出来,然后在剩余的特征上重复这个过程,直到所有特征都遍历了。这个过程中特征被消除的次序就是特征的排序。因此,这是一种寻找最优特征子集的贪心算法。
2.3.1.3 Embedded(集成法)
将特征选择嵌入到模型训练当中。其主要思想是通过使用某些机器学习的算法和模型进行训练,得到各个特征的权值系数,根据系数从大到小选择特征。常用方法有:基于惩罚项的特征选择法(L1、L2)、决策树的特征选择法(信息熵、信息增益)。
注:该方法与算法强相关,所以在算法实现的时候进行阐述。

2.3.2 PCA(主成分分析法)

主成分分析(Principal components analysis,PCA)是一种分析、简化数据集的技术。主成分分析经常用于减少数据集的维数,同时保持数据集中的对方差贡献最大的特征(旨在找到数据中的主成分,并利用这些主成分表征原始数据,从而达到降维的目的)。这是通过保留低阶主成分,忽略高阶主成分做到的。

一、 优缺点

  1. 优点:

    • 降低数据的复杂性,识别最重要的多个特征
    • 仅需方差衡量信息量,不受数据集以外的因素影响
    • 各主成分之间正交,可消除原始数据成分间的相互影响的因素
    • 计算方法简单,主要运算式特征值分解,易于实现
  2. 缺点:

    • 可能损失有用信息(由于没有考虑数据标签,容易将不同类别数据完全混合在一起,很难区分)

二适用数据类型——数值型数据

2.3.3 LDA(线性判别分析法)

LDA是一种监督学习的降维技术,它的数据集的每个样本是有类别输出的。PCA与此不同,PCA是不考虑样本类别输出的无监督降维技术。LDA的思想是“最大化类间距离和最小化类内距离”(将数据在低维度上进行投影,投影后希望每一种类别数据的投影点尽可能的接近,而不同类别的数据的类别中心之间的距离尽可能的大)
  1. 优点:

    • 在降维过程中可以使用类别的先验知识经验
    • LDA在样本分类信息依赖均值而不是方差的时候,比PCA之类的算法较优
  2. 缺点:

    • LDA不适合对非高斯分布(非正态分布)样本进行降维
    • LDA降维后可降为[1, 2,……,k-1]维,其中k为类别数
    • LDA在样本分类信息依赖方差而不是均值的时候,降维效果不好
    • LDA可能过度拟合数据

参考文献

  1. 特征工程到底是什么
  2. LDA和PCA降维
  3. 机器学习实战

1.如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到

2.关注公众号执鸢者,领取学习资料(前端“多兵种”资料),定期为你推送原创深度好文

查看原文

赞 0 收藏 0 评论 0

执鸢者 发布了文章 · 1月2日

前端不得不懂的架构知识(下)

关注公众号“执鸢者”,回复“资料”获取500G资料(各“兵种”均有),还有专业交流群等你一起来潇洒。(哈哈

成为一名伟大的架构师是所有程序猿的梦想,身为前端切图仔的我也不例外,近期在拜读这方面的书籍,确实写得太好了,并进行了相关总结,分享给各位,互相学习,共同进步!本系列博文将分为三期,这是第三期,主要阐述可扩展模式、传统可扩展架构模式、微服务、微内核架构、互联网技术演进的模式、互联网架构模板。

一、可扩展模式

二、传统可扩展架构模式

2.1 分层架构

2.2 SOA(面向服务的架构)

三、微服务

3.1 SOA与微服务对比

3.2 微服务的陷阱

3.3 最佳实践

3.4 基础设施

四、微内核架构

4.1 两类组件

4.2 设计关键点

4.3 OSGi

4.4 规则引擎架构

五、互联网技术演进的模式

六、互联网架构模板

6.1 互联网标准技术架构

6.2 存储层技术

6.3 开发层技术

6.4 服务层技术

6.5 网络层技术

6.6 用户层技术

6.7 业务层技术

6.8 平台技术

相关文章

前端不得不懂的架构知识(上)
前端不得不懂的架构知识(中)

参考文献

从0开始学架构

1.如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到

2.关注公众号执鸢者,领取学习资料(前端“多兵种”资料),定期为你推送原创深度好文
image

查看原文

赞 1 收藏 0 评论 0

执鸢者 发布了文章 · 2020-12-13

前端不得不懂的架构知识(中)

关注公众号“执鸢者”,回复“资料”获取500G资料(各“兵种”均有),还有专业交流群等你一起来潇洒。

成为一名伟大的架构师是所有程序猿的梦想,身为前端切图仔的我也不例外,近期在拜读这方面的书籍,并进行了相关总结,分享给各位,互相学习,共同进步!本系列博文将分为三期,这是第二期,主要阐述高性能缓存架构、单服务器高性能模式、高性能负载均衡、CAP定理、FMEA方法、高可用存储架构、计算高可用架构、业务高可用架构。

一、高性能缓存架构

1.1 缓存架构设计要点

二、单服务器高性能模式

2.1 PPC

2.2 TPC

2.3 Reactor

2.3 Proactor

三、高性能负载均衡

3.1 为什么需要高性能集群

3.2 负载均衡分类

3.3 使用原则

3.4 根据算法期望达到目的分类

四、CAP定理

五、FMEA方法

六、高可用存储架构

6.1 高可用存储架构分类

6.1.1 主备

6.1.2 主从

6.1.3 主主

6.1.4 集群

6.1.5 分区

七、计算高可用架构

7.1 复杂度来源

7.2 架构设计关键点

7.3 分类

7.3.1 主备

7.3.2 主从

7.3.3 集群

八、业务高可用架构

8.1 异地多活架构

8.1.1 判断是否满足异地多活

8.1.2 实现异地多活代价

8.1.3 异地多活设计4大技巧

8.1.4 异地多活设计步骤

  1. 业务分级

  1. 数据分类

  1. 数据同步

  1. 异常处理

8.2 应对接口级的故障

8.2.1 导致接口级故障原因

8.2.2 解决方案

相关文章

前端不得不懂的架构知识(上)

参考文献

从0开始学架构

1.如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到

2.关注公众号执鸢者,领取学习资料(前端“多兵种”资料),定期为你推送原创深度好文

image

查看原文

赞 2 收藏 2 评论 0

执鸢者 发布了文章 · 2020-12-06

一文彻底搞懂前端监控

关注公众号“执鸢者”,回复“红宝书”获取“javaScript高级程序第四版(pdf)”及大量前端学习资料。

一、前端监控现状

近年来,前端监控是越来越火,目前已经有很多成熟的产品供我们选择使用,如下图所示

有这么多监控平台,那为什么还要学习搞前端监控?

  • 一方面人家是要钱的
  • 另一方面自己的项目需要定制化的功能。

二、前端监控的目的

  1. 提升用户体验
  2. 更快的发现发现异常、定位异常、解决异常
  3. 了解业务数据,指导产品升级——数据驱动的思想

三、前端监控的流程

3.1 采集

前端监控的第一个步骤就是数据采集,采集的信息包含环境信息、性能信息、异常信息、业务信息。

3.1.1 环境信息

环境信息是每个监控系统必备的内容,毕竟排查问题的时候需要知道来自哪个页面、浏览器是谁、操作用户是谁……,这样才能快速定位问题,解决问题。一般这些常见的环境信息主要包含:
  1. url:正在监控的页面,该页面可能会出现性能、异常问题。获取方式为:<br/>
    window.location.href<br/>
  2. ua:访问该页面时该用户的userAgent信息,包含操作系统和浏览器的类型、版本等。获取方式为:<br/>
    window.navigator.userAgent
  3. token:记录当前用户是谁。通过记录该用户是谁。<br/>
    一方面方便将该用户的所有监控信息建立联系,方便数据分析;<br/>
    另一方面通过该标识可以查看该用户的所有操作,方便复现问题。<br/>

3.1.2 性能信息

页面的性能直接影响了用户留存率,,Google DoubleClick 研究表明:如果一个移动端页面加载时长超过 3 秒,用户就会放弃而离开。BBC 发现网页加载时长每增加 1 秒,用户就会流失 10%。,Google DoubleClick 研究表明:如果一个移动端页面加载时长超过 3 秒,用户就会放弃而离开。BBC 发现网页加载时长每增加 1 秒,用户就会流失 10%。所以我们的追求就是提高页面的性能,为了提高性能需要监控哪些指标呢?

3.1.2.1 指标分类

指标有很多,我总结为以下两个方面:网络层面和页面展示层面。
一、网络层面
从网络层面来看涉及的指标有:重定向耗时、DNS解析耗时、TCP连接耗时、SSL耗时、TTFB网络请求耗时、数据传输耗时、资源加载耗时……,各个指标的解释如下表所示:
指标解释
重定向耗时重定向所耗费的时间
DNS解析耗时浏览器输入网址后首先会进行DNS解析,其可以对服务器是否工作作出反馈
TCP连接耗时指建立连接过程的耗时
SSL连接耗时指数据安全性、完整性建立耗时
TTFB 网络请求耗时表示浏览器接收第一个字节的时间
数据传输耗时浏览器接收内容所耗费的时间
资源加载耗时DOM构建完毕后到页面加载完毕这段时间

二、页面展示层面

页面展示层面的指标是针对用户体验提出的几个指标,包含FP、FCP、LCP、FMP、DCL、L等,这几个指标其实就是chrome浏览器中performance模块的指标(如图所示)。

各个指标的解释如下表所示。

指标解释
FP(First Paint)首次绘制,标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点.
FCP(First Contentful Paint)首次内容绘制,标记浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 元素.
LCP(Largest Contentful Paint)最大内容渲染,表示可视区“内容”最大的可见元素开始出现在屏幕上的时间点。
FMP(First Meaningful Paint)首次有效绘制,表示页面的“主要内容”开始出现在屏幕上的时间点。它是我们测量用户加载体验的主要指标。
DCL(DomContentLoaded)当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载.
L(onLoad)当依赖的资源全部加载完毕之后才会触发
TTI(Time to Interactive)可交互时间,用于标记应用已进入视觉渲染并能可靠响应用户输入的时间点
FID(First Input Delay)首次输入延迟,用户首次和页面交互(单击链接、点击按钮等)到页面响应交互的时间

3.1.2.2 指标求解

上述这么多指标该怎么获取呢?浏览器给我们留了相应的接口——神奇的window.performance,通过该接口可以获取一些列与性能相关的参数,下面以https://baidu.com 为例来看一下与这些指标相关的参数:

window.performance中的timing属性中的内容不就是为了求解上述指标所需要的值吗?看着上面的属性值再对应下面的performance访问流程图,整个过程是不是一目了然。

有了上面的值我们就一起求解上述的指标:
一、网络层面
指标计算
重定向耗时redirectEnd - redirectStart
DNS解析耗时domainLookupEnd - domainLookupStart
TCP连接耗时connectEnd - connectStart
SSL连接耗时connectEnd - secureConnectionStart
TTFB 网络请求耗时responseStart - requestStart
数据传输耗时responseEnd - responseStart
资源加载耗时loadEventStart - domContentLoadedEventEnd

二、页面展示层面

Google工程师一直在推动以用户为中心的性能指标,所以页面展示层面的变化较大,求解方式稍有不同:
  1. FP和FCP

通过window.performance.getEntriesByType(‘paint’)的方式获取

const paint = window.performance.getEntriesByType('paint');
const FP = paint[0].startTime,
const FCP = paint[1].startTime,
  1. LCP
function getLCP() {
    // 增加一个性能条目的观察者
    new PerformanceObserver((entryList, observer) => {
        let entries = entryList.getEntries();
        const lastEntry = entries[entries.length - 1];
        observer.disconnect();
        console.log('LCP', lastEntry.renderTime || lastEntry.loadTime);
    }).observe({entryTypes: ['largest-contentful-paint']});
}
  1. FMP
function getFMP() {
    let FMP;
    new PerformanceObserver((entryList, observer) => {
        let entries = entryList.getEntries();
        observer.disconnect();
        console.log('FMP', entries);
    }).observe({entryTypes: ['element']});
}
  1. DCL
domContentLoadEventEnd – fetchStart
  1. L
loadEventStart – fetchStart
  1. TTI
domInteractive – fetchStart
  1. FID
function getFID() {
    new PerformanceObserver((entryList, observer) => {
        let firstInput = entryList.getEntries()[0];
        if (firstInput) {
            const FID = firstInput.processingStart - firstInput.startTime;
            console.log('FID', FID);
        }
        observer.disconnect();
    }).observe({type: 'first-input', buffered: true});
}

3.1.3 异常信息

对于网站来说,异常信息是最致命、最影响用户体验的问题,需要重点监控。对于异常信息可以分为两类:运行时错误、接口错误。下面就分别来唠一唠这两类错误。

一、运行时错误

当JavaScript运行时有可能会发生错误,可归类为七种:语法错误、类型错误、范围错误、引用错误、eval错误、URL错误、资源加载错误。为了捕获代码错误,需要考虑两类场景:非Promise场景和Promise场景,因为两种场景捕获错误的策略不同。

1.非Promise场景

非Promise场景可通过监听error事件来捕获错误。对于error事件捕获的错误分为两类:资源错误和代码错误。资源错误指的就是js、css、img等未加载,该错误只能在捕获阶段获取到,且为资源错误时event.target.localName存在值(用此区分资源错误与代码错误);代码错误指的就是语法错误、类型错误等这一类错误,可以获取代码错误的信息、堆栈等,用于排查错误。
export function listenerError() {
    window.addEventListener('error', (event) => {
        if (event.target.localName) {
            console.log('这是资源错误', event);
        }
        else {
            console.log('这是代码错误', event);
        }
    }, true)
}

2.Promise场景

Promise场景的处理方式有所不同,当Promise被reject且没有reject处理器的时候,会触发unhandlerejection事件,所以通过监听unhandlerejection的事件来捕获错误。
export function listenerPromiseError() {
    window.addEventListener('unhandledrejection', (event) => {
        console.log('这是Promise场景中错误', event);
    })
}

二、接口错误

对于浏览器来说,所有的接口均是基于XHR和Fetch实现的,为了捕获接口中的错误,可以通过重写该方法,然后通过接口返回的信息来判断当前接口的状况,下面以XHR为例来展示封装过程。
function newXHR() {
    const XMLHttpRequest = window.XMLHttpRequest;
    const oldXHROpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = (method, url, async) => {
        // 做一些自己的数据上报操作
        return oldXHROpen.apply(this, arguments);
    }

    const oldXHRSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = (body) => {
        // 做一些自己的数据上报操作
        return oldXHRSend.apply(this, arguments);
    }
}

3.1.4 业务信息

每个产品都会有自己的业务信息,例如用户在线时长、pv、uv、用户分布等,通过获取这些业务信息才能更加清楚的了解目前产品的状况,以便产品经理更好的去规划产品的未来方向。由于每个产品业务信息多种多样,小伙伴本可以按照自己的需求进行撰写代码,此处我就不再赘述。

3.2上报

对于上报的方式无外乎两种:一种是Ajax的方式上报;另一种是通过Image的形式进行上报。目前很多大厂采用的上报方式均是通过一个1*1像素的的gif图片进行上报,既然人家都采用该种策略,那我们就来唠一唠下面两个问题。
  • 为什么采用Image的方式上报?<br/>

    1. 没有跨域问题。因为数据服务器和后端服务器大概率是不同的域名,若采用Ajax的方式进行处理还要处理跨域问题,否则数据会被浏览器拦截。<br/>
    2. 不会阻塞页面加载,只需new Image对象即可。
  • 图片类型很多,为什么采用gif这种格式进行上报?<br/>
    其实归结为一个字——小。对于1*1px的图片,BMP结构的文件需要74字节,PNG结构的文件需要67字节,GIF结构的文件只需要43字节。同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量,所以选择gif进行上报。

3.3分析

日志上报之后需要进行清洗,获取自己所需要内容,并将分析内容进行存储。根据数据量的大小可分为两种方式:单机和集群。

一、单机<br/>

访问量小、日志少的网站可以采用单机的方式对数据进行分析,例如用node读取日志文件,然后通过日志文件中获取所需要的信息,最终将处理的信息存储到数据库中。<br/>

二、集群<br/>

很多产品的访问量很大,日志很多,此时就需要利用Hadoop进行分布式处理,获取最终处理结果,其处理流程图如下所示:

根据自己的日志量级决定自己的分析方式,合适的就是最好的,不用一味追求最优的、最先进的处理方式。

3.4报警

当异常类型超多一定阈值之后需要进行报警通知,让对应的工作人员去处理问题,及时止损。根据报警的级别不同,可以选择不同的报警方式。
  1. 邮件——普通报警
  2. 短信——严重报警,已影响部分业务
  3. 电话——特别严重,例如系统已宕机

1.如果觉得这篇文章还不错,来个分享、点赞、吧,让更多的人也看到

2.关注公众号执鸢者,领取学习资料,定期为你推送原创深度好文

参考

http://www.alloyteam.com/2020...
https://www.colabug.com/2019/...
image

查看原文

赞 1 收藏 1 评论 0

执鸢者 发布了文章 · 2020-11-22

三步法解析Axios源码

关注公众号“执鸢者”,回复“红宝书”获取“javaScript高级程序第四版(pdf)”及大量前端学习资料。

一、领悟思想

Axios是一个基于Promise的HTTP库,根据官网介绍,有以下几个特点:
  1. 在浏览器端会创建XMLHttpRequests
  2. 在Node端会创建HTTP请求
  3. 由于Axios是一个基于Promise的HTTP库,所以其支持Promise API
  4. 支持请求和响应拦截器
  5. 支持请求和响应数据转换
  6. 支持取消请求
  7. 自动转换JSON数据
  8. 客户端支持防御XSRF攻击
通过上述官网介绍的特点,我认为其突出的优点有三个:
  1. 支持Promise API,可以方便进行链式调用;
  2. 支持请求和响应拦截器,该拦截器将Node中中间件思想引入该库,在请求发送之前和响应接收之后可以对其进行处理。
  3. 支持数据转换器,转换器主要负责数据发送前以及响应接收后对数据的处理。

二、把握设计

理解了该库设计的特点,下面从源码目录、抽象接口及核心设计原理三个层面对Axios进行整体的把握。

2.1 源码目录

如下所示是Axios的源码目录及各个文件的作用

2.2 抽象接口

对源码的目录有了一定了解,下面利用UML类图对该系统各个模块的依赖关系进一步了解,为后续源码分析打好基础。(看该图注意对着源码一起看)

2.3 设计原理

首先看一段代码,这段代码的执行顺序包含着Axios的核心原理。
axios.defaults.baseURL = 'http://localhost:8080'

// 请求拦截器一
axios.interceptors.request.use(
    config => {
        console.log('请求拦截器一', config);
        return config;
    },
    error => {
        console.log('request interceptor rejected1');
        return Promise.reject(error);
    }
);

// 请求拦截器二
axios.interceptors.request.use(
    config => {
        console.log('请求拦截器二', config);
        return config;
    },
    error => {
        console.log('request interceptor rejected2');
        return Promise.reject(error);
    }
);

// 响应拦截器一
axios.interceptors.response.use(
    response => {
        console.log('响应拦截器一', response);
        return response;
    },
    error => {
        console.log('response interceptor rejected1');
        return Promise.reject(error);
    }
);

// 响应拦截器二
axios.interceptors.response.use(
    response => {
        console.log('响应拦截器二', response);
        return response;
    },
    error => {
        console.log('response interceptor rejected2');
        return Promise.reject(error);
    }
);

axios('/', {
    method: 'post',
    headers: {
        'Content-Type': 'application/json'
    },
    data: {
        test: 'test'
    },
    // 请求转换器
    transformRequest: [(data, headers) => {
        console.log('请求转换器', data);
        return JSON.stringify(data)
    }],
    // 响应转换器
    transformResponse: [(response, headers) => {
        console.log('响应转换器', response);
        return response;
    }]
})
.then((response) => {
    console.log(response.data)
}) 
写了这么多代码,大家肯定对这段代码的执行结果很感兴趣,为了满足各位看客的好奇心,下面就直接抛出来这段结果。

不过单看执行结果也不能了解其核心设计原理呀,老铁别急,其实小小代码就已经包含了Axios的整个执行过程,通过观察结果及代码可以将整个过程简化为下图:

其核心原理就是这个吗?是的,你没有看错,这就是Axios的核心设计原理,通过一系列链式的处理就能够得到所需要的结果。

三、体会细节

宏观的事聊完了,下面就详细聊几个核心细节吧:整个流程、请求/响应拦截器、dispatchRequest是个啥、请求/响应数据转换器。

3.1 整体运行流程

在第二章中阐述了该核心原理,老铁们一定对该整体是如何运转起来的很感兴趣吧,下面就来解答各位老铁的疑惑——Axios
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  // 拦截器实例化
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// 通过一系列的继承绑定操作,该函数其实就是axios函数
Axios.prototype.request = function request(config) {
  // ……
  config = mergeConfig(this.defaults, config);

  // Set config.method
  // ……

  // ****核心****
  // 存储该调用链的数组
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 将请求拦截器的内容塞到数组前面(注意用的unshift函数,这就很好的解释了为什么先调用的请求拦截器后执行)
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // 将响应拦截器的内容塞到数组后面
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  // 利用Promise将整个数组中的内容串起来,这样就可以按照顺序链式执行了
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
}; 
是不是很巧妙?通过利用数组先来存储需要的内容,先处理的在数组的前面(请求拦截器),后处理的在数组的后面(响应拦截器),然后利用Promise将整个内容串起来,很好的处理网络请求属于异步的问题——Perfect。

3.2 请求/响应拦截器

通过观察第二部分的执行结果我们已经了解了请求/响应拦截器,下面就做一下总结:
  1. 请求拦截器就是在发送请求前执行的回调函数,个人认为其最大功用就是对多个请求的配置进行统一修改
  2. 仔细观察发现请求拦截器一先加入但是后执行,是不是与整体运行流程中的代码对上了。
  3. 响应拦截器就是在请求得到响应后执行的回调函数,成功回调的参数就是响应response,其可以对多个请求的响应进行统一修改。
先抛出请求/响应拦截器的核心代码
function InterceptorManager() {
  this.handlers = [];
}

// 注册拦截器
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

// 删除拦截器
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

// 对拦截器进行分发
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
}; 
看看拦截器的核心源码,是不是发现与一种设计模式很像?对的,就是观察者模式。当调用use方法的时候就会将回调函数(成功、失败)保存至handlers属性上,方便后期的调用;当调用eject方法的时候就会删除对应索引位置回调函数;当调用forEach方法的时候就会就会对handlers属性(存储的拦截器回调)中的内容进行分发。

3.3 dispatchRequest是个啥

前面聊了整个请求的请求前(请求拦截器)和请求后(响应拦截器),是不是感觉少点东西,如何发请求,这就是我们本次要与大家一起唠的dispatchRequest(config)。
module.exports = function dispatchRequest(config) {
  // ……

  //请求数据转换
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
  // ……
  
  // 获取适配器:自己配置了就选自己的,自己没有设置就选默认的(浏览器端就选xhrAdapter、node端就选httpAdapter;这也就是为什么Axios即支持浏览器又支持Node的原因)
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    // ……

    // 响应数据转换器
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      // ……

      // 响应数据转换器
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
}; 
通过观察整个请求流程中的中间环节——dispatchRequest,它一共做了三件事:
  1. 调用请求数据转换器转换请求数据
  2. 选择合适的适配器发起请求——自己配置了就选自己的,自己没有配置就选默认的(浏览器端就选xhrAdapter、node端就选httpAdapter;这也就是为什么Axios即支持浏览器又支持Node的原因)
  3. 当请求数据返回后,调用响应数据转换器转换响应数据

3.4 请求/响应数据转换器

既然3.3中提到了请求/响应转换器,本节就来聊一聊它俩。
// 核心源码
module.exports = function transformData(data, headers, fns) {
  utils.forEach(fns, function transform(fn) {
    data = fn(data, headers);
  });

  return data;
}; 
请求数据转换调用,实质上就是利用请求数据转换器对请求头和请求数据进行特定的处理(transformRequest为处理函数的数组,defaults中包含默认的配置)
config.data = transformData(
  config.data,
  config.headers,
  config.transformRequest
); 
响应数据转换调用类似于请求数据转换调用,对响应体进行一系列的处理(transformResponse为处理函数的数组,defaults中包含默认的配置)
response.data = transformData(
  response.data,
  response.headers,
  config.transformResponse
); 

四、结语

上述三章对Axios进行整体的分析,从Axios的特点、整体设计及关键环节三个方面进行了讲述,通过阅读源码学到了很多知识,也能够更加熟练的使用Axios。为了保证各位老铁的学习Axios源码的效果,对学习Axios源码的两条建议:
  1. 边阅读本文边看源码,能够有更深入的理解。
  2. 不要纠结于具体的实现,从宏观的角度去看源码,这样能够节省大量时间。

1.如果觉得这篇文章还不错,来个分享、点赞、吧,让更多的人也看到

2.关注公众号执鸢者,领取学习资料,定期为你推送原创深度好文
image

查看原文

赞 0 收藏 0 评论 0

执鸢者 发布了文章 · 2020-11-15

硬核知识点——浏览器中的三类五种请求

关注公众号“执鸢者”,回复“红宝书”获取“javaScript高级程序第四版(pdf)”及大量前端学习资料。

对浏览器的请求进行划分,可以分为三类:一般请求、Ajax请求、WebSocket请求,对于每种请求都有不同的产生方式,今天就以这个思想为主线来一起唠一唠。

一、一般请求

此处说的一般请求就是指浏览器会直接显示响应体数据,这些请求会刷新\跳转页面。换个更加容易理解的说法吧,指的就是控制台Network面板中除了XHR和WS部分显示的请求。例如js、css、img资源。

二、Ajax请求

Ajax请求也是由浏览器发出,但是不会对界面进行任何操作,只是调用监视的回调函数并传入响应相关数据,发出Ajax请求可以通过三种方式:XHR、Fetch、Axios,其余的均不是Ajax请求。

2.1 XHR

最早将Ajax推到历史舞台的关键技术就是XMLHttpRequest(XHR)对象,虽然目前已经有了一些过时的嫌疑,但是还是很有必要提一下它。下面就按照一个请求的整个生命周期来看一看该技术。

一、 对象的实例化

既然要使用XHR,第一步就是要将该对象实例化
const xhr = new XMLHttpRequest();

二、初始化操作

将对象实例化后是不是紧接着就需要进行初始化操作,到底该请求要发给谁、通过什么请求发、该请求到底是同步发还是异步发
xhr.open(method, url, async)

三、请求头设置

了解网络的同学本肯定知道请求头的概念,既然要与后端打交道,请求头还是有必要进行设置的(默认的配置不一定满足我们高大上的需求),例如想发送json格式的内容,这个时候就需要设置Content-Type为application/json
xhr.setRequestHeader('Content-Type', 'application/json');

四、接收请求的准备工作

浏览器除了设置常见的请求头外,还需要指定响应数据类型,得到响应后好自动解析。目前支持的类型有string、arraybuffer、blob、document、json、text、ms-stream。
xhr.responseType('json')

五、发送请求

前期工作都准备好了,接下来就是激动人心的时刻了,看好呀,要按开始键发送请求啦。
xhr.send(data)

六、监听响应

我喊一声美女,人家肯定要回应一下呀,毕竟颜值在这,不回应该是多么不给面子的一件事呀!!!为了等待人家的回应,则需要分三步进行:
  1. 进入监听状态,放在这就是通过onreadystatechange进行监听。
  2. 等待正面回应。readyStatus表征目前的状态,当readyStatus为4(请求完成),响应算是接收到了
  3. 处理响应。不能一股脑的处理全部响应吧,毕竟也是要面子的人,我肯定只希望接收我喜欢的信息吧,就喜欢状态码在200~299之间的,别的一概pass掉。
xhr.onreadystatechange = () => {
    if (xhr.readyState == 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
            console.log(xhr.response);
        }
    }
}

七、中断请求

正常流程算是走完了,肯定还有非正常流程,发起请求后我后悔了,不想得到对方的回应了,此时仍然后办法——中断请求
xhr.abort()
注:本文不是文档学习,详细使用请见https://developer.mozilla.org...

2.2 Fetch

长江后浪推前浪,互联网技术发展这么快,出现了新的技术(Fetch)能够执行XMLHttpRequest对象的所有任务,该技术使用更容易,接口更现代化,能够在Web工作线程等现代Web工具中使用。(Fetch必须是异步,XMLHttpRequest可同步可异步)。
const payload = JSON.stringify({
    test: 'test'
});

let headersObj = new Headers({
    'Content-Type':'application/json'
});

let request = new Request('http://localhost:8080');

fetch(request, {
    method: 'POST',
    body: payload,
    headers: headersObj
})
.then((response) => response.json())
.then(console.log)
上述代码虽然简单,但是已经囊括了Fetch API中所有的概念:fetch、Headers、Request、Response、Body混入。
  1. fetch()
fetch()方法暴露在全局作用域中,包括主页面执行线程、模块和工作线程,调用该方法,浏览器就会向给定URL发送请求。<br/>
(1)fetch(input[, init]):接收两个参数,input为要获取的资源,__init为一个配置对象,配置需要传入的参数,满足更多复杂的需求<br/>
(2)返回一个promise对象,从而链式的进行处理
  1. Headers
相当于 response/request 的头信息,可以使你查询到这些头信息,或者针对不同的结果做不同的操作。该对象包含检索、设置、添加、删除,设置完自己需要的头信息后就可以将其挂载到fetch中的配置信息中。
  1. Request
该对象是获取资源请求的接口,暴露了请求和相关信息。可以将该对象的实例作为fetch函数中的第一个参数
  1. Response
该对象是获取资源响应的接口,并暴露了响应的相关信息。
  1. Body混入
提供了与 response/request 中的 body 有关的方法,可以定义它的内容形式以及处理方式。在Body混入中提供了5个方法,用于将ReadableStream转存到缓冲区的内存中,将缓冲区转换为某种JavaScript对象类型,以及通过Promise产生结果。

(1)Body.text():返回Promise,解决将缓冲区转存得到的UTF-8格式字符串

(2)Body.json():返回Promise,解决将缓冲区转存得到的JSON

(3)Body.formData():返回Promise,解决将缓冲区转存得到的FormData实例

(4)Body.arrayBuffer():返回Promise,解决将缓冲区转存得到的ArrayBuffer

(5)Body.text():返回Promise,解决将缓冲区转存得到的Blob实例

2.3 Axios

Axios应该是目前前端最流行的Ajax请求库,具有以下特点:

  1. 基于Promise的异步Ajax请求库
  2. 浏览器端/node端都可以使用
  3. 支持请求/响应拦截器
  4. 支持请求取消
  5. 请求/响应数据转换
  6. 批量发送请求

对于Axios还是比较有意思的,本次只说一下其简单使用,下一期准备剖析一下其源码,有兴趣的小伙伴可以先搬好小板凳占个坑,关注一下。

// 默认配置
axios.defaults.baseURL = 'http://localhost:8080'

// 请求拦截器
axios.interceptors.request.use(
    config => {
        console.log('request interceptor resolved');
        return config;
    },
    error => {
        console.log('request interceptor rejected');
        return Promise.reject(error);
    }
);

// 响应拦截器
axios.interceptors.response.use(
    response => {
        console.log('response interceptor resolved');
        return response;
    },
    error => {
        console.log('response interceptor rejected');
        return Promise.reject(error);
    }
);

let cancel; // 用于保存取消请求的函数
axios('/', {
    method: 'post',
    headers: {
        'Content-Type': 'application/json'
    },
    data: {
        test: 'test'
    },
    // 取消请求
    cancelToken: new axios.CancelToken((c) => {
        cancel = c;
    })
})
.then((response) => {
    console.log(response.data)
})

// 若想取消请求,直接调用下面函数
// cancel();
上述代码已经囊括了Axios库中大多数核心内容,包括axios()函数、默认设置、请求/响应拦截器、取消请求(内部设计的很巧妙,想知道的请看下期讲解)
  1. axios()
完成相应配置并发送请求,调用方式有多种语法糖,同学们可以按需使用。
  1. 默认设置
通过axios.defaults.xxx可以完成很多全局配置,提高代码的复用。(提高复用真是完美的编码思想)
  1. 请求/响应拦截器
请求拦截器的作用就是在请求发送之前先进行一些列的处理;响应拦截器的作用就是触发请求的回调之前执行响应拦截器,对响应做一些预处理操作
  1. 取消请求
通过配置cancelToken对象并缓存用于取消请求的cancel函数,在需要的时候触发该函数取消请求(内部其实就是调用的xhr.abort())

对于更多使用见详细使用文档https://github.com/axios/axios

三、WebSocket请求

下面来聊聊这个传奇协议——WebSocket,WebSockt通过一个长时连接实现与服务器全双工、双向的通信。(特别提醒:同源策略不适用于WebSocket)
let ws = new WebSocket('ws://127.0.0.1:8080');

// 在连接建立成功时
ws.onopen = () => {
    ws.send('websocket')
}

// 在接收到消息时
ws.onmessage = (event) => {
    console.log(event.data);
}

// 在发生错误时
ws.onerror = () => {
    console.log('error');
}

// 在连接关闭时
ws.onclose = () => {
    console.log('close');
}
上述代码已经囊括大部分WebSocket的概念,实例化WebSocket建立与服务端的连接;通过事件监听即可了解WebSokcet连接目前的状态;通过send()函数即可向服务端发送内容;当服务端发送消息时即可触发message事件,通过event.data属性获取其有效载荷。

本篇文章虽然比较简单,但是可以帮助我们认清楚请求其实是分为三类的,这是我最最最大的收获,欢迎小伙伴们能够给出自己的想法。

1.如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到

2.关注公众号执鸢者,领取学习资料,定期为你推送原创深度好文

查看原文

赞 0 收藏 0 评论 0

执鸢者 发布了文章 · 2020-11-08

理论与实践相结合彻底理解CORS

关注公众号“执鸢者”,回复“红宝书”获取“javaScript高级程序第四版(pdf)”及大量前端学习资料。

跨域问题一直是面试中的经典问题,不管是前端老鸟还是新鸟都碰到过。其中针对跨源Ajax请求中有一个终极解决办法——CORS(跨源资源共享)大家肯定也不陌生,一说这个名词,我们就会哗啦哗啦说出来一套又一套的理论知识,但是这些理论知识很多我们做的仅仅是去背诵,很少去验证每一个理论点,本节我们将通过实验的方式去验证这些理论点,通过理论与实践相结合的方式彻底理解CORS。

一、理论知识

既然是CORS,背背这些理论点肯定不为过吧,我就用三幅图对这个理论进行一些简单的总结

1.1 请求类型

1.1.1 简单请求

1.1.2 非简单请求

1.2 请求如何带上Cookie信息

二、实验

为实验做好前期准备工作,包含一个html页面和一个服务器程序,其中html访问网址为http://127.0.0.1:8009; 服务器监听端口为:8010.
  1. html页面初始代码
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>test CORS</title>
    </head>
    <body>
        CORS
        <script data-original="https://code.bdstatic.com/npm/axios@0.20.0/dist/axios.min.js"></script>
        <script>
            axios('http://127.0.0.1:8010', {
                method: 'get'
            }).then(console.log)
        </script>
    </body>
</html>
  1. 服务器端代码(用express框架)
const express = require('express');

const app = express();

app.get('/', (req, res) => {
    console.log('get请求收到了!!!');
    res.send('get请求已经被处理');
})
app.listen(8010, () => {
    console.log('8010 is listening')
});

2.1 实验一

实验目的:

  1. 非同源会产生跨域问题
  2. 跨域是浏览器对响应拦截造成的
  1. 首先来看看浏览器控制台内容

控制台内容显示报错,报错内容是跨域,这是因为端口不同(一个8009一个8010),两者不同源,所以导致跨域,验证了实验目的1.
  1. 紧接着来瞅瞅服务器控制台打印了啥内容

控制台内容打印了接收到了get请求,则证明浏览器的请求发出了并被服务器端正常接收,从侧面证明了跨域是浏览器对响应进行了拦截,从而验证了实验目的2.

2.2 实验二

实验目的

  1. 服务器配置Access-Control-Allow-Origin会解决跨域问题
  2. 浏览器通过响应头中是否包含Access-Control-Allow-Origin这个响应头的值与请求头中Origin是否相等来确定是否能够进行跨域访问
  1. 首先来搂一眼请求头

  1. 紧接着再来搂一眼响应头

按照理论来说,请求头中的Origin字段表示本次请求来自哪个源(协议+域名+端口),服务器根据这个值来决定是否同意这次请求。如果Origin指定的源不在许可范围内,服务器会返回一个正常的HTTP回应。目前并没有修改,还处于报错状态,该响应确实是一个正常响应,不包含Access-Control-Allow-Origin字段,但是这也不足以说服我浏览器是通过验证该字段来确实是否允许跨域请求。所以紧接着需要做一个对比试验,通过修改服务端的代码后观察响应头内容。
  1. 从最简单的修改开始,直接将响应头中加入Access-Control-Allow-Origin=“http://127.0.0.1:8009” 字段,理论上来说此时会允许所有的跨域请求。
服务端代码修改后内容
app.get('/', (req, res) => {
    console.log('get请求收到了!!!');
    res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8009');
    res.send('get请求已经被处理');
})
响应头内容

观察到响应头中多了一行内容:Access-Control-Allow-Origin:http://127.0.0.1:8009 字段,在看看响应内容,确实有消息返回了,内容如下:

  1. 只验证了Origin和Access-Control-Allow-Origin中内容响应,若内容不同又会有什么现象呢?
服务端代码进一步修改
app.get('/', (req, res) => {
    console.log('get请求收到了!!!');
    res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8008');
    res.send('get请求已经被处理');
})
响应头内容

此时浏览器控制台报错了,出现了跨域问题

通过该实验可以验证通过配置Access-Control-Allow-Origin字段可以解决跨域问题;此外,浏览器是通过检查响应头中Access-Control-Allow-Origin字段的值与Origin的值是否相等来确定是否允许跨域访问的。通过该实验达到了我们实验的目的。

2.3 实验三

实验目的
验证CORS请求默认不发送Cookie信息,如果要把Cookie发送到服务器,一方面要服务器同意(通过指定Access-Control-Allow-Origin字段且Access-Control-Allow-Origin需要指定具体域名);另一方面浏览器请求中必须带上withCredentials字段。
  1. 通过观察请求头(看实验一中请求头),并不包含Cookie信息
  2. 代码修改
index.html页面进行修改
axios('http://127.0.0.1:8010', {
    method: 'get',
    withCredentials: true
}).then(console.log)
服务器端代码修改
app.get('/', (req, res) => {
    console.log('get请求收到了!!!');
    console.log('cookie 内容为', req.headers.cookie);
    res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8009');
    res.setHeader('Access-Control-Allow-Credentials', true);
    res.cookie('test', 'test', {expires: new Date(Date.now() + 900000)});
    res.send('get请求已经被处理');
})
  1. 再次观察请求头内容,带上了cookie

  1. 看看服务器端有没有接收到cookie信息,控制台信息如下,确实收到了cookie信息

按照上述进行配置发送请求过程中将会带着cookie信息,上一个配置将会报错(可以自行验证)

2.4 实验四

实验目的

  1. 验证非简单请求会增加一次预检请求
  2. 预检请求是Options请求
  3. 请求头中会携带非简单请求的请求方法(Access-Control-Request-Methods)和头信息(Access-Control-Request-Headers),预检请求的响应头信息中Access-Control-Allow-Methods和Access-Control-Allow-Headers与上述请求头中的信息匹配才可以发送正常的CORS请求。
  1. 第一步肯定是要修改代码了
index.html代码
axios('http://127.0.0.1:8010', {
    method: 'post',
    headers: {
        'Content-Type': 'application/json'
    },
    data: {
        name: 'dog'
    }
}).then(console.log)
服务器代码修改如下
app.options('/', (req, res) => {
    console.log('options请求收到了!!!');
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
    res.setHeader('Access-Control-Max-Age', 10000);
    res.send('options请求已经被处理');
});

app.post('/', (req, res) => {
    console.log('post请求收到了!!!');
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.send('post请求已经被处理');
});
  1. 修改完了代码是不是要瞅瞅结果呢?
先看看浏览器是不是输出了内容,确实有内容输出了

再瞅瞅服务器输出了些什么内容

可以看到本来打算发一次请求,但实际上发了两条,第一条是Options请求,第二条请求才是post请求,上述打印内容验证了实验目的中的一和二。

  1. 下面继续深入思考,来看看预检请求的请求头和响应头
请求头内容

响应头内容

上述Access-Control-Request-Headers与Access-Control-Allow-Headers一样,而且内容也正常返回了(步骤二中已经进行了展示),但是这不足以证明实验目的三,下面我们认为增加一条头信息再来看结果。

  1. 人为增加一条请求头信息
index.html页面修改后如下
axios('http://127.0.0.1:8010', {
    method: 'post',
    headers: {
        'Content-Type': 'application/json',
        'Test': 'test'
    },
    data: {
        name: 'dog'
    }
}).then(console.log)
此时浏览器控制台报错了

服务端只接收到了options请求

请求头信息为

响应头信息为

通过该实验证明只有Access-Control-Request-Headers与Access-Control-Allow-Headers相等的时候,预检请求才会通过,后续请求才会发出,从而达到了该实验的实验目的三。

1.如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到

2.关注公众号执鸢者,领取学习资料,定期为你推送原创深度好文

image

image.png

查看原文

赞 0 收藏 0 评论 0

执鸢者 发布了文章 · 2020-10-31

前端不得不懂的架构知识(上)

关注公众号“执鸢者”,回复“红宝书”获取“javaScript高级程序第四版(pdf)”,回复“架构”获取本博文对应书籍,还有专业交流群等你一起来潇洒。

成为一名伟大的架构师是所有程序猿的梦想,身为前端切图仔的我也不例外,近期在拜读这方面的书籍,并进行了相关总结,分享给各位,互相学习,共同进步!本系列博文将分为三期,这是第一期,主要阐述架构的基础知识、架构复杂度来源、架构设计的原则、架构设计流程、高性能数据库集群、高性能NoSQL。

一、基础

二、架构复杂度来源

架构复杂度主要来源于六个方面:高性能、高可用、可扩展性、成本、安全、规模,下面让我们一起来研究一下这六个方面。

2.1 高性能

2.2 高可用

2.3 可扩展性

2.4 成本

2.5 安全

2.6 规模

三、架构设计的原则

四、架构设计流程

五、高性能数据库集群

高性能数据库集群的方式有两种:读写分离和分库分表。读写分离的本质是将访问压力分散到集群中的多个节点,但是没有分散存储压力;分库分表既可以分散访问压力,又可以分散存储压力。

5.1 读写分离

5.2 分库分表

六、高性能NoSQL

1.如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到

2.关注公众号执鸢者,领取学习资料,定期为你推送原创深度好文
image.png
image

查看原文

赞 0 收藏 0 评论 0

执鸢者 发布了文章 · 2020-10-24

前端更应懂产品

关注公众号“执鸢者”,回复“红宝书”获取“javaScript高级程序第四版(pdf)”并进入专业交流群.
个人观点:前端工程师是最接近用户的研发工程师,我的观点就是“前端更应懂产品”。有人肯定会质疑我的观点,产品不是产品经理的事情,我们敲代码的懂这个有什么用?我们程序员懂技术不就行了吗?的确,技术对我们来说很重要,但是我认为产品对程序员(特别是前端工程师)也很重要。一方面毕竟产品是技术变现的途径,没有变现怎么来维护公司正常运营,给我们提供学习技术的土壤;另一方面学习产品知识能够为我们的职业生涯续命——(某位前辈说的,我也很认同)。本着这种想法,自己一直想了解一些产品的知识,可是没有合适的契机。近期一位“同学”给我推荐了“腾讯8分钟产品课”,收获良多,算是自己入坑产品的第一门课程。(纯属个人观点,不喜勿喷
本文将利用八张图从用户、定位、需求、时机、匠心、危机、合作、商业化八个方面对整门课进行了总结,并加入一些自己的理解,与诸君共同交流、学习、进步,欢迎诸位留言交流。

一、用户

开发一款产品目的是服务于用户,说白了用户就是我们的金主“爸爸”,所以开发一款产品前应该做的就是定义我们的用户群体并从用户的角度去考虑问题,这样才能让我们的产品拥有更大的市场——归结一句就是从用户思维考虑问题。

二、定位

每一个产品开发初期都存在一个定位,后期的业务拓展都将服务该定位。就拿我们团队来说,核心是为媒体服务(为媒体投放广告),后期开展的工作都是围绕该核心进行,例如开发了很多智能工具,其目的是为了媒体进行最优投放。

三、需求

每一个产品都是为了解决人们生活中的痛点问题,所以我们应该有一双善于发现的眼睛,发现用户的心理诉求,让用户变的更方便、更舒适、更“懒”。基于我们发现的需求,按照需求的紧急程度依次完成。

四、时机

说白了就是要了解外部环境。就拿前端工程师找工作来说,目前市场上需要的是技术栈是React、Vue,你面试的时候再谈jQuery是不是就有点与外部环境脱节了,如果你是后者,显然没有了解目前的技术环境。

五、匠心

每一位工程师对待自己的产品都应该有工匠精神,不断进行打磨、不断进行优化、不断做的更加傻瓜好用,这样才能做到业界领先。

六、危机

生于忧患死于安乐,时刻保持危机感,不断去了解我们产品当前的状况,基于内外部环境变换去优化产品,才能让产品的生命周期更长。(此处有个疑问,短视频行业的未来是不是AR、VR、全息投影?)

七、合作

合作的前提条件是利益,找到共赢点才能更好的合作。

八、商业化

不管是谁,开发一款产品的目的肯定是为了赚钱,考虑好该产品的盈利点并获得利益才能正向的促进该产品的“进化”——(产品赚钱如果不损害用户体验该多好,感觉微信朋友圈的广告就很好,不仔细看根本看不出是广告)

欢迎大家关注公众号(回复“书籍”获取大量前端学习资料,回复“前端视频”获取大量前端教学视频)
1.如果觉得这篇文章还不错,来个分享、点赞、吧,让更多的人也看到
2.关注公众号执鸢者,领取学习资料,定期为你推送原创深度好文

image
查看原文

赞 3 收藏 2 评论 0

认证与成就

  • 获得 339 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-02-21
个人主页被 2k 人浏览