路游远方

路游远方 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

路游远方 收藏了文章 · 10月8日

vue全家桶+Echarts+百度地图,搭建数据可视化系统

本文章篇幅略长,内容有点多
大佬可根据目录选择性查阅
新人可一步步来阅读

1 前言

1.1 业务场景

突然接到产品说要做一个数据监控的系统。有线图、柱状图、地图,类似于数据可视化的方式。

本人之前从未接触过Echarts,然后需要2周拿出成果,有点慌😂😂

1.2 业务分析

拿到需求看了一下

支持用户名、密码登录,默认显示一个维度数据,然后点击可钻取进入第二维度数据,再点击进入第三维度数据展示。

大致估摸着。。。

  1. 系统搭建vue-cli
  2. vuex记录登录信息
  3. vue-router路由跳转
  4. 3个维度的页面,提取出共用的组件
  5. 各个组件开发
  6. 调节样式,增加UI
  7. 加入后台接口数据
  8. 优化显示
  9. 测试
  10. 上线

当然这不是要2周内,全做完。应该是完成步骤6。

相对于公司就我一个前端,没接触过Echarts,有问题都没人讨论的情况下。。。

心里还是忍不住想吐槽一下😒😒😒

1.3 效果展示

这里列出了第一维度页面的样式。文字请无视,哈哈。

image

2 系统安装

吐槽归吐槽,活还是要干的。😎

因为本人最熟悉的还是vue,所以还是选择了用vue全家桶来做。这部分可参考 vue build

2.1 安装node环境

  • 下载安装node环境,直接去官网下载即可 node.js
安装完后可在命令行运行node -vnpm -v 查看是否安装成功以及版本

2.2 安装Vue项目

2.2.1 安装vue

官方文档:vuejs

这里我们使用npm的方式

  • npm i vue

2.2.2 安装Vue CLI

官方文档:vue CLI

  • npm i -g @vue/cli
安装之后,你就可以在命令行中访问 vue 命令。你可以用这个命令来查看其版本。vue -V

2.2.3 创建项目

这里安装的时候,注意将我们要使用的安装上。vuexvue-router,其他可根据需要添加。

  1. 方法一
  • vue create hello-world
这里参照官方网站,有很详细的介绍。参照:vue create
  1. 方法二

使用图形化界面

  • vue ui
界面含中文,很好操作。参照:vue ui

2.2.4 安装插件

  1. 方法一

最直接也是推荐的 npm i xxx

这里介绍一下 -S -D -g 的区别

  • npm i -S xxx 文件写入dependencies,用于工程中开发时使用到的插件,会发布到生产环境如UI,JS等。
  • npm i -D xxx 文件写入devDependencies,用于工程支持类插件,不会发布到生产环境,如gulp等压缩打包工具。
  • npm i -g xxx 全局安装,如vue、ncu等。安装目录为:C:Users用户AppDataRoamingnpm
  1. 方法二

vue ui图像化界面中包含了若干插件,可点击安装,但不一定是最新版本。

同时会在hello中引入。其他和方法一没区别。

2.3 安装Echarts

这里我们为了方便,使用了npm全量引用,后期为了精简项目可单个引用。
  • npm i echarts -S

然后在main.js中添加

image

这里建议提前自定义echarts的样式,并引入到项目中。官方自定义地址:theme-builder

在页面中我们可以如下引用:
var myChart = this.$echarts.init(document.getElementById("myid"),'temp')

myid是我们要展示的echartsid

temp是我们的自定义的样式,同时官方提供了几个样式例子,可以node_modules\echarts\theme中找到。

2.4 安装element-ui

这里我们为了方便,使用了npm全量引用,后期为了精简项目可单个引用。
  • npm i element-ui -S

然后在main.js中添加

image

2.5 安装百度地图

一般vue使用百度地图有2种方式,

  • 一种是像官网那样去应用。如:BMap
  • 第二种是使用 vue-baidu-map

不管是哪一种都要去申请账号和密钥。申请地址为:百度地图密钥(ak)

这里我使用了第二种。vue-baidu-map文档

  • npm i vue-baidu-map -S

然后在main.js中添加

image

xxxxxxxx这里填写自己申请的密钥。

在页面中,参照文档,可使用标签<baidu-map/>来调用。

2.6 初始化样式

css样式初始化,简单来说就是为了各个浏览器能统一什么的。

这里我使用的是 normalize.css

下载下来后,在main.js中添加

image

基本上的准备工作都做好了,接下来就是具体的代码了。

3 项目搭建

3.1 router、vuex

我这里新建了一个router.jsstore.js,大致如下:

image

哦哈,这里路由定义是为了方便看哈,具体还是根据业务来定义。

这里的router.beforeEach 路由卫士用于是否登录校验。

然后我们在main.js中来引用。

image

更多请参考官方文档:Vue RouterVuex

3.2 Login页面

登录页面没啥,就是个form提交,由于路由中判断user.id。所以我们储存一下,然后跳转到Index页面就行。

这里只是一种方式,也可以使用CookieSession

3.3 Index页面

分析页面分成了2个大部分

  • 第一部分是 头部
  • 第二部门是 主体

image

我们将头部当做一个组件进行复用。

组件的复用可参考官方文档:https://cn.vuejs.org/v2/guide...

3.4 header页面

头部比较简单,除了一些显示外,还有一个显示当前时间。

这里我们使用了setInterval,每隔1秒去获取一下当前时间赋值给你定义的v-model就行。

同时在离开页面时,我们清理掉定时器。

这里需要我们对Vue生命周期有一定的了解。

获取当前时间的方法可参考:data-module.js

3.5 主体页面

这里分析一下页面,主要分成了3块。

  1. 左边,包含了2个折线图。
  2. 中间,包含了数字和地图。
  3. 右边,包含了柱状图和表格。
接下来主要介绍一下,自己这2周摸索出来的一些Echarts配置,适合新手,大佬轻喷。

这里需要经常翻阅 Echarts配置项API

3.5.1 Echarts基本

这里列出基本的渲染写法,具体的图形和数据只要修改option就可以了。

<template>
    <div id="myecharts" class="myecharts">
</template>

<script>
    export default {
        mounted(){
            this.drawECharts()
        },
        
        methods:{
            drawECharts(){
                // temp 是我们的自定义样式,上面安装Echarts时有介绍
                var myChart = this.$echarts.init(document.getElementById("myecharts"),'temp')
                var option = {}
                option = {
                    // 吧啦吧啦 一堆配置
                }
                // 执行渲染图形和数据的操作
                if (option && typeof option === "object") {
                    myChart.setOption(option, true)
                }
            }
        }
    }
</script>

<style>
    //  一定要设置大小,不然不出来,这玩意和canvas一样
    .myecharts{
        width : 500px;
        height : 300px;
    }
</style>

3.5.2 线形图

多多实践,就会发现每个配置和其参数的作用了。

option = {
    // 提示框(就是鼠标放上去后出现的框)
    tooltip : {
        trigger: 'axis',
        axisPointer: {
            type: 'cross',
            label: {
                backgroundColor: '#6a7985'
            }
        }
    },
    
    //  右上角的组件,用于说明,也可进行点击筛选
    legend: {
        align : 'right',
        x : 'right',
        top : 25,
        selectedMode : 'single',    //  我这里设置的单选模式
        data:['好','坏']            //  显示的第一项和第二项,这里的颜色是根据自定义主题的颜色顺序来定的
    },
    
    //  x、y轴显示一些设置,比如刻度颜色显示什么的,可在自定义主题设置一部分
    xAxis: {
        type: 'category',
        boundaryGap: false,
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    },
    yAxis: {
        type: 'value'
    },
    // 具体配置项,根据具体项目查看官网配置项说明
    series: [
        {
            name : '好',
            data: [150, 132, 201, 534, 290, 530, 820],
            type: 'line',
            smooth: true,   //  是否平滑曲线
            areaStyle: {},
        },
        { 
            name : '坏',
            data: [82, 93, 90, 93, 129, 333, 432],
            type: 'line',
            smooth: true,
            areaStyle: {},
        }
    ]
}

// 查看Echarts的API,我们需要显示默认的一些数据,配置如下
// 默认显示坏的数据
myChart.dispatchAction({
    type: 'legendSelect',
    name: '坏',
})
//  默认显示第7个数据
myChart.dispatchAction({
    type: 'showTip',
    seriesIndex: 1,
    dataIndex: 6,
})

预览:

image

3.5.3 柱状图

这里我们直接用双柱状图来演示。

因为名字和数字需要提示和点击的功能,所以没有使用echartsy轴。

不然formatter又要写一堆,虽然用了自定义的,但最开始是用的formatter

可以实现相同展示,但无法操作,如鼠标提示和鼠标点击。

<div class="left">
    <div v-for="it in its1" :key="it.id">
        <el-tooltip class="item"  effect="light" placement="bottom-start">
            <div slot="content">名称:{{it.name}}<br/>个数:{{it.num}}</div>
            <div class="name" @click="go"> {{ it.name.substring(0,4)+'...' }}</div>
        </el-tooltip>
        <div class="num">{{ it.num }}</div>
    </div>
</div>

<div class="right">
    <div v-for="it in its2" :key="it.id">
        <el-tooltip class="item"  effect="light" placement="bottom-start">
            <div slot="content">名称:{{it.name}}<br/>个数:{{it.num}}</div>
            <div class="name" @click="go"> {{ it.name.substring(0,4)+'...' }}</div>
        </el-tooltip>
        <div class="num">{{ it.num }}</div>
    </div>
</div>

<div id="myecharts" class="myecharts">

css这里就不贴了,效果就是这2行文字刚好贴在2行柱状图前面。

接下来是echarts配置。

option = {
    // 鼠标提示框
    tooltip: {
        trigger: 'axis',
        axisPointer: {
            type: 'shadow'
        }
    },
    
    // 右边显示
    legend: {
        selectedMode:false,
        data: ['好', '坏'],
        top:5,
        right:8,
    },
    
    // 两个图坐标的位置
    grid: [
        {left: '16%', top:'10%', width: '22%', height: '86%'},
        {left: '65%', top:'10%', width: '22%', height: '86%'}
    ],
    
    // 两个图x轴的设置,这里的gridIndex就是个序号,用于区分
    xAxis: [
        {gridIndex : 0, show : false},
        {gridIndex : 1, show : false},
    ],
    
    // 两个图y轴的设置,注释的部分是用echarts本身的y轴来显示名称和数量的
    yAxis: [
        {
            gridIndex: 0,
            type: 'category',
            show : false,
            data : ['广东/12','杭州/13','北京北京/14','天津/16'],
            // axisLabel: {
            //   formatter : function(value){
            //     let arr = value.split('/')
            //     return '{a|'+arr[0]+'}\n{b|'+ arr[1]+'}';
            //   },
            //   rich: {
            //     a: {
            //       color : '#ffffff',
            //       lineHeight : 19,
            //       fontSize : 14,
            //       align: 'right',
            //     },
            //     b:{
            //       fontSize : 18,
            //       lineHeight : 19,
            //       fontWeight : 'bold',
            //       align: 'right',
            //       fontFamily : 'Digital',
            //     }
            //   }
            // }
        },
        {
            gridIndex: 1,
            show : false,
            type: 'category',
            data : ['海南/12','三亚/13','哈尔滨/14','西双版纳/16'],
            // axisLabel: {
            //   formatter : function(value){
            //     let arr = value.split('/')
            //     return '{a|'+arr[0]+'}\n{b|'+ arr[1]+'}';
            //   },
            //   rich: {
            //     a: {
            //       color : '#ffffff',
            //       lineHeight : 19,
            //       fontSize : 14,
            //       align: 'right',
            //     },
            //     b:{
            //       fontSize : 18,
            //       lineHeight : 19,
            //       fontWeight : 'bold',
            //       align: 'right',
            //       fontFamily : 'Digital',
            //     }
            //   }
            // }
        },
    ],
    
    //  渲染图形和数据,bar是柱状图
    //  barWidth 柱状的宽度
    //  两类两套,所以有4组数据,使用xAxisIndex、yAxisIndex来区分。
    series: [
        {
            name: '好',
            type: 'bar',
            barWidth  : 5,
            barMinHeight : 5,
            barGap : '100%',
            xAxisIndex: 0,
            yAxisIndex: 0,
            data: [0, 3489, 9022234, 922228],
            label: {
                normal: {
                    position: 'right',
                    show: true
                }
            },
        },
        
        {
            name: '坏',
            type: 'bar',
            barWidth  : 5,
            barMinHeight : 5,
            xAxisIndex: 0,
            yAxisIndex: 0,
            data: [0, 2438, 3300, 1594],
            label: {
                normal: {
                    position: 'right',
                    show: true
                }
             },
        },
        {
            name: '好',
            type: 'bar',
            barWidth  : 5,
            barMinHeight : 10,
            barGap : '100%',
            xAxisIndex: 1,
            yAxisIndex: 1,
            data: [8203, 3489, 9034, 222],
            label: {
                normal: {
                    position: 'right',
                    show: true
                }
            },
        },
        {
            name: '坏',
            type: 'bar',
            barWidth  : 5,
            barMinHeight : 5,
            xAxisIndex: 1,
            yAxisIndex: 1,
            data: [445, 2438, 3300, 555],
            label: {
                normal: {
                    position: 'right',
                    show: true
                }
            },
        },
    ]
}

预览:

image

3.5.4 表格

table我这里使用了element-ui加上修改 UI 默认css 和 滚动条的 css

这里列出一项,其他写法相似。

<el-table
    :data="tableData"
    height="252"
    style="min-width: 100%;">
    <el-table-column
        prop="date"
        min-width="12"
        header-align="center"
        label="时间">
        <template slot-scope="scope">
            <template v-if="scope.row.if == '1'">
                <img data-original="../../assets/img/new.png"/>
                <div style="color:#E63136;margin-top:-27px;margin-left:35px;">
                    {{scope.row.date}}
                </div>
            </template>
            <template v-else>
                <div style="margin-left:35px;">
                    {{ scope.row.date }}
                </div>
            </template>
        </template>
    </el-table-column>
</el-table>
  • UI 样式

UI css的修改,这里我使用了自定义字体哦,完全copy是不起作用的。

其他的设置项不做说明,F12打开,随便玩。

.el-table thead {
    color: #FFFFFF;
}

.el-table {
    color: #00A5F6;
    font-family: 'Regular';
    background-color: rgba(0, 0, 0, 0.03);
    th {
        padding: 2px 0;
        background-color: #003260; 
    }
    th.is-leaf {
        border-bottom: 0px solid #EBEEF5;
    }
    tr {
        background-color: rgba(0, 0, 0, 0.03);
    }
    td {
        border-bottom: 1px solid #2c3547;
        padding: 2px 0;
    }
    
    .el-table::before {
        height: 0px;
        z-index: 0;
        background-color: #2c3547;
    }
}
  • 滚动条的样式

随意改变看看效果就懂了,谷歌浏览器 😅

/* scrollbar */

::-webkit-scrollbar {
  width: 8px;
  height: 1px;
  background-color:transparent;
}
::-webkit-scrollbar-thumb {
  border-radius: 10px;
  background: #adabab;
}
::-webkit-scrollbar-track {
  border-radius: 10px;
  background:#394d63;
}

预览:

image

3.5.5 百度地图

vue-baidu-map 文档
<baidu-map 
    :center="map.center"        //  地图中心经纬 {lng: 114.023598, lat: 33.589299}
    :scroll-wheel-zoom="true"   //  地图是否滚轮缩放
    :zoom="map.zoom"            //  默认地图尺寸
    :mapStyle="mapStyle"        //  地图样式
    class="baidumap">           //  地图宽高
    <template v-for="(it,index) in ms">     //  标点
        <bm-marker 
            :key="it.id" 
            :position="it.position"         //  标点位置
            @click="markclick(it,index)"    //  标点点击事件
            @mouseover="markover(it,index)" //  鼠标移动到标点上的事件
            :icon="it.if? iocn:newincon"    //  标点的样式
            @mouseout="markout(it,index)">  //  鼠标从标点移走的事件
            <bm-info-window
                :show="it.show"             //  标点提示框的显示true/false
                :position="it.position">    //  提示框坐标
                <p v-text="it.mess"></p>    //  提示框内容
            </bm-info-window>
        </bm-marker>
    </template>
</baidu-map>

预览:
image

3.5.6 矢量地图

Echarts矢量地图的类型有

  • type:'scatter' 散点气泡图,可在地图中显示不用颜色程度的点
  • type:'effectScatter' 有涟漪特效动画的散点图
  • type:'map' 地理区域的数据可视化
  • type:'lines' 地图航线、路线的可视化

引入地图

require('echarts/map/js/china.js')
require('echarts/map/js/province/beijing.js')

这里我有问题,我要引入全国的省份,就要多写30多个require,有没有大佬能给出更好的办法?

找到一个全量引用的方法

const rjs = require.context('echarts/map/js/province')
rjs.keys().forEach(rjs)

可以替代

require('echarts/map/js/province/beijing.js')
require('echarts/map/js/province/shanxi.js')
require('echarts/map/js/province/neimenggu.js')
等等等。。。

地图配置:

option = {
    //  鼠标提示
    tooltip : {
        trigger: 'item',
        formatter : function(params){
            var val = params.data
            return '名称:'+val.name+',个数:'+val.value[2]+'<br/>'+'总数:'+val.tol+',个数:'+val.un
        },
    },
    
    //  不同颜色的点
    visualMap: [
        {
            min: 0,
            max: 1,
            show : false,
            inRange: {
                color: ['#01cae2', '#e63136',]
            },
            dimension : 3,
        },
    ],

    // 地图样式
    geo: {
        map: 'china',   // 地图样式,当为‘北京’时,会显示北京地图   
        roam : true,    
        label: {
            emphasis: {
                show: true
            }
        },
        zoom : 1.2,     //  初始大小
        scaleLimit : {
            min : 1.2,  //  最小缩放
            max : 6     //  最大缩放
        },
        regions : regions(data)     // 省份样式方法
    },

    series : [
        {
            name: '分布',
            type: 'scatter',
            coordinateSystem: 'geo',    //  地图配置
            data: convertData(data.sort(function (a, b) {   //  数据方法
                return b.value - a.value;
            })),
            encode: {
                value : 2
            },
            hoverAnimation: true,
            itemStyle: {
                normal: {
                    // color: '#FF3030',
                    shadowBlur: 1,
                }
            },
        }
    ]
};

当点击省份地图时,我们可以进入省份地图的矢量图

myChart.on('click',function(params){
    option.geo.map =  '北京'
    myChart.setOption(option, true);
})

预览:

image


image

4 后记

目前还没有接入后端数据,就是全前台的一个展示。很多都是全量引入,后续待成熟后,可慢慢精简。

总结一下:Vue入门 + Echarts入门 ,希望能对你有用!!!😜😜😜

感谢支持。若不足之处,欢迎大家指出,共勉。

如果觉得不错,记得 点赞,谢谢大家 😂

欢迎关注 我的:【Github】【掘金】【简书】【CSDN】【OSCHINA】【SF】

本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

出处为:https://github.com/xrkffgg/Tools

查看原文

路游远方 赞了文章 · 2019-08-18

React V16.9来了 无痛感升级 加入性能检测 【译-真香】

2019年8月8日,我们发布了React 16.9。它包含几个新功能,错误修正和新的弃用警告,以帮助准备未来的主要版本。

clipboard.png

新的版本:

重命名不安全的生命周期方法

一年多以前,我们宣布重新命名不安全的生命周期方法:

componentWillMount → UNSAFE_componentWillMount
componentWillReceiveProps → UNSAFE_componentWillReceiveProps
componentWillUpdate → UNSAFE_componentWillUpdate

React 16.9不包含重大更改,旧版本名称在此版本中继续有效。但是,当您使用任何旧名称时,您将看到警告:

警告:componentWillMount已重命名,不建议使用。

正如警告所示,每种不安全方法通常都有更好的方法。但是,您可能没有时间迁移或测试这些组件。在这种情况下,我们建议运行一个“codemod”脚本,自动重命名它们:


npx react-codemod rename-unsafe-lifecycles

(注意它说npx,不是npm。npx默认情况下是Node 6+附带的实用程序。)

运行此codemod将替换旧名称,如componentWillMount新名称,如UNSAFE_componentWillMount

Codemod在行动

新的名字 UNSAFE_componentWillMount 将继续在React 16.9和React 17.x中运行。但是,新的UNSAFE_前缀将帮助具有问题模式的组件在代码审查和调试会话期间脱颖而出。(如果您愿意,可以通过选择严格模式进一步阻止他们在您的应用中使用。)

注意

详细了解我们的版本政策和对稳定性的承诺。

弃用javascript:网址

以...开头的URL javascript:是一个危险的攻击面,因为它很容易在标签中意外包含未经过类型化的输出 a标签,并创建一个安全漏洞:

const userProfile = {
  website: "javascript: alert('you got hacked')",
};
// This will now warn:
<a href={userProfile.website}>Profile</a>

在React 16.9中,此模式继续有效,但它将记录警告。如果您使用javascript:URL作为逻辑,请尝试使用React事件处理程序。(作为最后的手段,你可以绕过保护dangerouslySetInnerHTML,但是它非常气馁并经常导致安全漏洞。)

在未来的主要版本中,如果遇到javascript:URL , React将抛出错误。

弃用“工厂”组件

在使用Babel编译JavaScript类之前变得流行之前,React支持使用render方法返回对象的“工厂”组件:

function FactoryComponent() {
  return { render() { return <div />; } }
}

这种模式令人困惑,因为它看起来太像一个功能组件 - 但它不是一个。(函数组件只会<div />在上面的例子中返回。)

这种模式几乎从未在野外使用,并且支持它会导致React略大且比必要的慢。因此,我们在16.9中弃用此模式,并在遇到警告时记录警告。如果您依赖它,添加FactoryComponent.prototype = React.Component.prototype可以作为一种解决方法。或者,您可以将其转换为类或函数组件。

我们不希望大多数代码库受此影响。

新功能

异步act()测试

React 16.8引入了一个新的测试实用程序,act()用于帮助您编写更符合浏览器行为的测试。例如,一次act()获取批量内的多个状态更新。这与React在处理真实浏览器事件时的工作方式相匹配,并有助于为将来React将更频繁地批量更新的组件做好准备。

但是,在16.8中act()仅支持同步功能。有时,您可能在测试中看到过类似的警告但无法轻松修复它:

An update to SomeComponent inside a test was not wrapped in act(...).
在React 16.9中,act()也接受异步函数,你可以await调用它:

await act(async () => {
  // ...
});

这解决了act()以前无法使用的其余情况,例如状态更新在异步函数内部时。因此,您应该能够立即修复act()测试中的所有剩余警告。

我们听说没有足够的信息来说明如何编写测试act()。新的“ 测试食谱”指南介绍了常见的场景,以及如何act()帮助您编写好的测试。这些示例使用vanilla DOM API,但您也可以使用React Testing Library来减少样板代码。它的许多方法已在act()内部使用。

如果您碰到任何其他不适合您的情况,请告知我们问题跟踪器act(),我们会尽力提供帮助。

性能测量 <React.Profiler>

在React 16.5中,我们为DevTools引入了一个新的React Profiler,它可以帮助您找到应用程序中的性能瓶颈。在React 16.9中,我们还添加了一种编程方式来收集所谓的测量<React.Profiler>。我们预计大多数较小的应用都不会使用它,但在较大的应用中跟踪性能回归可能很方便。

<Profiler>如何往往是一个作出反应的应用程序呈现什么渲染的“成本”的措施。其目的是帮助识别应用程序的某些部分,这些部分很慢并且可能会受益于优化(如memoization)。

<Profiler>可以在React树中的任何位置添加A 来测量渲染树的该部分的成本。它需要两个道具:一个id(字符串)和一个onRender回调(函数),当树中的一个组件“提交”更新时,它会调用它。

render(
  <Profiler id="application" onRender={onRenderCallback}>
    <App>
      <Navigation {...props} />
      <Main {...props} />
    </App>
  </Profiler>
);

要了解更多有关Profiler并传递给参数onRender回调,检查出的Profiler文档。

注意:

分析会增加一些额外的开销,因此在生产构建中禁用它。

为了选择生产分析,React提供了一个特殊的生产构建,并启用了分析。阅读有关如何在fb.me/react-profiling中使用此构建的更多信息。

值得注意的错误修正
此版本包含一些其他显着的改进:

修复findDOMNode()了在<Suspense>树内调用时崩溃的问题。

保留删除的子树导致的内存泄漏也已得到修复。

由setStatein 引起的无限循环useEffect现在记录错误。(这类似于你看,当你调用错误setState中componentDidUpdate的一类。)

我们感谢所有帮助解决这些问题和其他问题的贡献者。您可以在下面找到完整的更改日志。

路线图的更新
在2018年11月,我们发布了16.x版本的路线图:

带有React Hooks的小型16.x版本(过去估计:2019年第一季度)
带有并发模式的小型16.x版本(过去的估计:2019年第二季度)
带有Suspense for Data Fetching的未成年人16.x版本(过去估计:2019年中)
这些估计太乐观了,我们需要调整它们。

tldr:我们按时发布了Hooks,但我们正在将Concurrent Mode和Suspense for Data Fetching重新组合成一个我们打算在今年晚些时候发布的版本。

2月份,我们发布了一个稳定的16.8版本,包括React Hooks,一个月后 React Native支持。但是,我们低估了此版本的后续工作,包括lint规则,开发人员工具,示例和更多文档。这使时间线改变了几个月。

现在React Hooks已经推出,并行模式和数据提取的悬念工作正在全面展开。目前正在积极开发的新Facebook网站建立在这些功能之上。使用真实代码对它们进行测试有助于在影响开源用户之前发现并解决许多问题。其中一些修复涉及这些功能的内部重新设计,这也导致时间线滑落。

有了这种新的理解,这就是我们计划下一步做的事情。

一个发行而不是两个
Concurrent Mode和Suspense 为正在积极开发的新Facebook网站提供支持,因此我们有信心他们在技术上接近稳定状态。我们现在也更好地了解了它们为开源采用做好准备之前的具体步骤。

最初我们认为我们会将Concurrent Mode和Suspense for Data Fetching分成两个版本。我们发现这种排序很难解释,因为这些特征与我们最初想到的相关性更大。因此,我们计划在单个组合版本中发布对Concurrent Mode和Suspense for Data Fetching的支持。

我们不希望再次过度推销发布日期。鉴于我们在生产代码中依赖于它们,我们希望今年能够提供16.x版本,并为其提供选择支持。

数据提取的更新
虽然React并未就如何获取数据发表意见,但数据提取的Suspense的第一个版本可能会专注于与固定数据提取库集成。例如,在Facebook,我们正在使用与Suspense集成的即将推出的Relay API。我们将记录像Apollo这样的其他自以为是的图书馆如何支持类似的整合。

在第一个版本中,我们不打算关注我们在早期演示中使用的临时“触发HTTP请求”解决方案(也称为“React Cache”)。但是,我们希望我们和React社区将在首次发布后的几个月内探索该空间。

服务器渲染的更新
我们已经开始研究新的支持Suspense的服务器渲染器,但是我们不希望它为初始版本的并发模式做好准备。但是,此版本将提供一个临时解决方案,允许现有服务器呈现器立即为Suspense回退发出HTML,然后在客户端上呈现其真实内容。这是我们目前在Facebook上使用的解决方案,直到流式渲染器准备就绪。

为什么需要这么长时间?
我们已经发布了导致Concurrent Mode稳定的各个部分,包括新的上下文API,延迟加载Suspense和Hooks。我们也急于释放其他缺失的部分,但是大规模地尝试它们是该过程的重要部分。诚实的回答是,当我们开始时,它只需要比我们预期的更多的工作。与往常一样,我们感谢您在Twitter和我们的问题跟踪器中提出的问题和反馈。

安装
应对
Npm注册表中提供了React v16.9.0。

要使用Yarn安装React 16,请运行:

yarn add react@^16.9.0 react-dom@^16.9.0
要使用npm安装React 16,请运行:

npm install --save react@^16.9.0 react-dom@^16.9.0
我们还通过CDN提供了反应的UMD版本:

<script crossorigin data-original="https://unpkg.com/react@16/um...;></script>
<script crossorigin data-original="https://unpkg.com/react-dom@1...;></script>
有关详细的安装说明,请参阅文档。

更新日志
应对
添加<React.Profiler>API以编程方式收集性能测量。(@bvaughn在#15172)
删除unstable_ConcurrentMode赞成unstable_createRoot。(@acdlite在#15532)
反应DOM
弃用UNSAFE_*生命周期方法的旧名称。(@bvaughn在#15186和@threepointone在#16103)
将javascript:URL 弃用为常见攻击面。(@sebmarkbage在#15047)
弃用不常见的“模块模式”(工厂)组件。(@sebmarkbage在#15145)
添加对disablePictureInPicture属性的支持<video>。(@eek in #15334)
添加对onLoad事件的支持<embed>。(@cherniavskii在#15614)
useState从DevTools 添加对编辑状态的支持。(@bvaughn在#14906)
添加对从DevTools切换Suspense的支持。(@gaeon在#15232)
setState从调用时发出警告useEffect,创建循环。(@gaeon在#15180)
修复内存泄漏。(@paulshen in #16115)
修复包含在其中findDOMNode的组件的内部崩溃<Suspense>。(@acdlite在#15312)
修复因刷新太晚而导致的待处理效果。(@acdlite在#15650)
修复警告消息中不正确的参数顺序。(@brickspert在#15345)
修复了存在!important样式时隐藏悬疑后备节点的问题。(@acdlite在#15861和#15882)
略微提高保湿性能。(@bmeurer在#15998)
反应DOM服务器
修复camelCase自定义CSS属性名称的错误输出。(@bedakb在#16167)
反应测试实用程序和测试渲染器
添加act(async () => ...)用于测试异步状态更新。(#14853中的@threepointone)
添加对act不同渲染器的嵌套的支持。(@threepointone在#16039和#16042)
如果在act()通话外安排效果,请在严格模式下警告。(@threepointone在#15763和#16041)
act从错误的渲染器使用时发出警告。(@threepointone在#15756)
编辑这个页面

欢迎加入我们的SegmentFault前端交流群~

目前人数太多,只能我邀请入群了哦~

clipboard.png

查看原文

赞 12 收藏 4 评论 1

路游远方 赞了文章 · 2019-08-18

深入理解Node.js 进程与线程(8000长文彻底搞懂)

前言

进程线程是一个程序员的必知概念,面试经常被问及,但是一些文章内容只是讲讲理论知识,可能一些小伙伴并没有真的理解,在实际开发中应用也比较少。本篇文章除了介绍概念,通过Node.js 的角度讲解进程线程,并且讲解一些在项目中的实战的应用,让你不仅能迎战面试官还可以在实战中完美应用。

文章导览

16c6cf612c275894?w=2772&h=1104&f=jpeg&s=377258

面试会问

Node.js是单线程吗?

Node.js 做耗时的计算时候,如何避免阻塞?

Node.js如何实现多进程的开启和关闭?

Node.js可以创建线程吗?

你们开发过程中如何实现进程守护的?

除了使用第三方模块,你们自己是否封装过一个多进程架构?

进程

进程Process是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,进程是线程的容器(来自百科)。进程是资源分配的最小单位。我们启动一个服务、运行一个实例,就是开一个服务进程,例如 Java 里的 JVM 本身就是一个进程,Node.js 里通过 node app.js 开启一个服务进程,多进程就是进程的复制(fork),fork 出来的每个进程都拥有自己的独立空间地址、数据栈,一个进程无法访问另外一个进程里定义的变量、数据结构,只有建立了 IPC 通信,进程之间才可数据共享。

  • Node.js开启服务进程例子
const http = require('http');

const server = http.createServer();
server.listen(3000,()=>{
    process.title='程序员成长指北测试进程';
    console.log('进程id',process.pid)
})

运行上面代码后,以下为 Mac 系统自带的监控工具 “活动监视器” 所展示的效果,可以看到我们刚开启的 Nodejs 进程 7663

16c4dc0ca13fec40?w=1406&h=1182&f=jpeg&s=131412

线程

线程是操作系统能够进行运算调度的最小单位,首先我们要清楚线程是隶属于进程的,被包含于进程之中。一个线程只能隶属于一个进程,但是一个进程是可以拥有多个线程的

单线程

单线程就是一个进程只开一个线程

Javascript 就是属于单线程,程序顺序执行(这里暂且不提JS异步),可以想象一下队列,前面一个执行完之后,后面才可以执行,当你在使用单线程语言编码时切勿有过多耗时的同步操作,否则线程会造成阻塞,导致后续响应无法处理。你如果采用 Javascript 进行编码时候,请尽可能的利用Javascript异步操作的特性。

经典计算耗时造成线程阻塞的例子

const http = require('http');
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e10; i++) {
    sum += i;
  };
  return sum;
};
const server = http.createServer();
server.on('request', (req, res) => {
  if (req.url === '/compute') {
    console.info('计算开始',new Date());
    const sum = longComputation();
    console.info('计算结束',new Date());
    return res.end(`Sum is ${sum}`);
  } else {
    res.end('Ok')
  }
});

server.listen(3000);
//打印结果
//计算开始 2019-07-28T07:08:49.849Z
//计算结束 2019-07-28T07:09:04.522Z

查看打印结果,当我们调用127.0.0.1:3000/compute
的时候,如果想要调用其他的路由地址比如127.0.0.1/大约需要15秒时间,也可以说一个用户请求完第一个compute接口后需要等待15秒,这对于用户来说是极其不友好的。下文我会通过创建多进程的方式child_process.forkcluster 来解决解决这个问题。

单线程的一些说明

  • Node.js 虽然是单线程模型,但是其基于事件驱动、异步非阻塞模式,可以应用于高并发场景,避免了线程创建、线程之间上下文切换所产生的资源开销。
  • 当你的项目中需要有大量计算,CPU 耗时的操作时候,要注意考虑开启多进程来完成了。
  • Node.js 开发过程中,错误会引起整个应用退出,应用的健壮性值得考验,尤其是错误的异常抛出,以及进程守护是必须要做的。
  • 单线程无法利用多核CPU,但是后来Node.js 提供的API以及一些第三方工具相应都得到了解决,文章后面都会讲到。

Node.js 中的进程与线程

Node.js 是 Javascript 在服务端的运行环境,构建在 chrome 的 V8 引擎之上,基于事件驱动、非阻塞I/O模型,充分利用操作系统提供的异步 I/O 进行多任务的执行,适合于 I/O 密集型的应用场景,因为异步,程序无需阻塞等待结果返回,而是基于回调通知的机制,原本同步模式等待的时间,则可以用来处理其它任务,

科普:在 Web 服务器方面,著名的 Nginx 也是采用此模式(事件驱动),避免了多线程的线程创建、线程上下文切换的开销,Nginx 采用 C 语言进行编写,主要用来做高性能的 Web 服务器,不适合做业务。

Web业务开发中,如果你有高并发应用场景那么 Node.js 会是你不错的选择。

在单核 CPU 系统之上我们采用 单进程 + 单线程 的模式来开发。在多核 CPU 系统之上,可以通过 child_process.fork 开启多个进程(Node.js 在 v0.8 版本之后新增了Cluster 来实现多进程架构) ,即 多进程 + 单线程 模式。注意:开启多进程不是为了解决高并发,主要是解决了单进程模式下 Node.js CPU 利用率不足的情况,充分利用多核 CPU 的性能。

Node.js 中的进程

process 模块

Node.js 中的进程 Process 是一个全局对象,无需 require 直接使用,给我们提供了当前进程中的相关信息。官方文档提供了详细的说明,感兴趣的可以亲自实践下 Process 文档。

  • process.env:环境变量,例如通过 process.env.NODE_ENV 获取不同环境项目配置信息
  • process.nextTick:这个在谈及 Event Loop 时经常为会提到
  • process.pid:获取当前进程id
  • process.ppid:当前进程对应的父进程
  • process.cwd():获取当前进程工作目录,
  • process.platform:获取当前进程运行的操作系统平台
  • process.uptime():当前进程已运行时间,例如:pm2 守护进程的 uptime 值
  • 进程事件:process.on(‘uncaughtException’, cb) 捕获异常信息、process.on(‘exit’, cb)进程推出监听
  • 三个标准流:process.stdout 标准输出、process.stdin 标准输入、process.stderr 标准错误输出
  • process.title 指定进程名称,有的时候需要给进程指定一个名称

以上仅列举了部分常用到功能点,除了 Process 之外 Node.js 还提供了 child_process 模块用来对子进程进行操作,在下文 Nodejs进程创建会继续讲述。

Node.js 进程创建

进程创建有多种方式,本篇文章以child_process模块和cluster模块进行讲解。

child_process模块

child_process 是 Node.js 的内置模块,官网地址:

child_process 官网地址:http://nodejs.cn/api/child_pr...

几个常用函数:
四种方式

  • child_process.spawn():适用于返回大量数据,例如图像处理,二进制数据处理。
  • child_process.exec():适用于小量数据,maxBuffer 默认值为 200 * 1024 超出这个默认值将会导致程序崩溃,数据量过大可采用 spawn。
  • child_process.execFile():类似 child_process.exec(),区别是不能通过 shell 来执行,不支持像 I/O 重定向和文件查找这样的行为
  • child_process.fork(): 衍生新的进程,进程之间是相互独立的,每个进程都有自己的 V8 实例、内存,系统资源是有限的,不建议衍生太多的子进程出来,通长根据系统 CPU 核心数设置。
CPU 核心数这里特别说明下,fork 确实可以开启多个进程,但是并不建议衍生出来太多的进程,cpu核心数的获取方式const cpus = require('os').cpus();,这里 cpus 返回一个对象数组,包含所安装的每个 CPU/内核的信息,二者总和的数组哦。假设主机装有两个cpu,每个cpu有4个核,那么总核数就是8。
fork开启子进程 Demo

fork开启子进程解决文章起初的计算耗时造成线程阻塞。
在进行 compute 计算时创建子进程,子进程计算完成通过 send 方法将结果发送给主进程,主进程通过 message 监听到信息后处理并退出。

fork_app.js
const http = require('http');
const fork = require('child_process').fork;

const server = http.createServer((req, res) => {
    if(req.url == '/compute'){
        const compute = fork('./fork_compute.js');
        compute.send('开启一个新的子进程');

        // 当一个子进程使用 process.send() 发送消息时会触发 'message' 事件
        compute.on('message', sum => {
            res.end(`Sum is ${sum}`);
            compute.kill();
        });

        // 子进程监听到一些错误消息退出
        compute.on('close', (code, signal) => {
            console.log(`收到close事件,子进程收到信号 ${signal} 而终止,退出码 ${code}`);
            compute.kill();
        })
    }else{
        res.end(`ok`);
    }
});
server.listen(3000, 127.0.0.1, () => {
    console.log(`server started at http://${127.0.0.1}:${3000}`);
});
fork_compute.js

针对文初需要进行计算的的例子我们创建子进程拆分出来单独进行运算。

const computation = () => {
    let sum = 0;
    console.info('计算开始');
    console.time('计算耗时');

    for (let i = 0; i < 1e10; i++) {
        sum += i
    };

    console.info('计算结束');
    console.timeEnd('计算耗时');
    return sum;
};

process.on('message', msg => {
    console.log(msg, 'process.pid', process.pid); // 子进程id
    const sum = computation();

    // 如果Node.js进程是通过进程间通信产生的,那么,process.send()方法可以用来给父进程发送消息
    process.send(sum);
})
cluster模块

cluster 开启子进程Demo

const http = require('http');
const numCPUs = require('os').cpus().length;
const cluster = require('cluster');
if(cluster.isMaster){
    console.log('Master proces id is',process.pid);
    // fork workers
    for(let i= 0;i<numCPUs;i++){
        cluster.fork();
    }
    cluster.on('exit',function(worker,code,signal){
        console.log('worker process died,id',worker.process.pid)
    })
}else{
    // Worker可以共享同一个TCP连接
    // 这里是一个http服务器
    http.createServer(function(req,res){
        res.writeHead(200);
        res.end('hello word');
    }).listen(8000);

}
cluster原理分析

16c5658b2e97e9b2

cluster模块调用fork方法来创建子进程,该方法与child_process中的fork是同一个方法。
cluster模块采用的是经典的主从模型,Cluster会创建一个master,然后根据你指定的数量复制出多个子进程,可以使用cluster.isMaster属性判断当前进程是master还是worker(工作进程)。由master进程来管理所有的子进程,主进程不负责具体的任务处理,主要工作是负责调度和管理。

cluster模块使用内置的负载均衡来更好地处理线程之间的压力,该负载均衡使用了Round-robin算法(也被称之为循环算法)。当使用Round-robin调度策略时,master accepts()所有传入的连接请求,然后将相应的TCP请求处理发送给选中的工作进程(该方式仍然通过IPC来进行通信)。

开启多进程时候端口疑问讲解:如果多个Node进程监听同一个端口时会出现 Error:listen EADDRIUNS的错误,而cluster模块为什么可以让多个子进程监听同一个端口呢?原因是master进程内部启动了一个TCP服务器,而真正监听端口的只有这个服务器,当来自前端的请求触发服务器的connection事件后,master会将对应的socket具柄发送给子进程。

child_process 模块与cluster 模块总结

无论是 child_process 模块还是 cluster 模块,为了解决 Node.js 实例单线程运行,无法利用多核 CPU 的问题而出现的。核心就是父进程(即 master 进程)负责监听端口,接收到新的请求后将其分发给下面的 worker 进程

cluster模块的一个弊端:

16c565aaeb065b4a?w=501&h=261&f=png&s=23033

cluster内部隐时的构建TCP服务器的方式来说对使用者确实简单和透明了很多,但是这种方式无法像使用child_process那样灵活,因为一直主进程只能管理一组相同的工作进程,而自行通过child_process来创建工作进程,一个主进程可以控制多组进程。原因是child_process操作子进程时,可以隐式的创建多个TCP服务器,对比上面的两幅图应该能理解我说的内容。

Node.js进程通信原理

前面讲解的无论是child_process模块,还是cluster模块,都需要主进程和工作进程之间的通信。通过fork()或者其他API,创建了子进程之后,为了实现父子进程之间的通信,父子进程之间才能通过message和send()传递信息。

IPC这个词我想大家并不陌生,不管那一张开发语言只要提到进程通信,都会提到它。IPC的全称是Inter-Process Communication,即进程间通信。它的目的是为了让不同的进程能够互相访问资源并进行协调工作。实现进程间通信的技术有很多,如命名管道,匿名管道,socket,信号量,共享内存,消息队列等。Node中实现IPC通道是依赖于libuv。windows下由命名管道(name pipe)实现,*nix系统则采用Unix Domain Socket实现。表现在应用层上的进程间通信只有简单的message事件和send()方法,接口十分简洁和消息化。

IPC创建和实现示意图

16c5b379ad12199e?w=391&h=311&f=png&s=23661

IPC通信管道是如何创建的

16c5b3812e3bb7d9?w=866&h=612&f=jpeg&s=103501

父进程在实际创建子进程之前,会创建IPC通道并监听它,然后才真正的创建出子进程,这个过程中也会通过环境变量(NODE_CHANNEL_FD)告诉子进程这个IPC通道的文件描述符。子进程在启动的过程中,根据文件描述符去连接这个已存在的IPC通道,从而完成父子进程之间的连接。

Node.js句柄传递

讲句柄之前,先想一个问题,send句柄发送的时候,真的是将服务器对象发送给了子进程?

子进程对象send()方法可以发送的句柄类型
  • net.Socket TCP套接字
  • net.Server TCP服务器,任意建立在TCP服务上的应用层服务都可以享受它带来的好处
  • net.Native C++层面的TCP套接字或IPC管道
  • dgram.Socket UDP套接字
  • dgram.Native C++层面的UDP套接字
send句柄发送原理分析

结合句柄的发送与还原示意图更容易理解。

16c5b52b15d87bbe?w=916&h=548&f=png&s=82815
send()方法在将消息发送到IPC管道前,实际将消息组装成了两个对象,一个参数是hadler,另一个是message。message参数如下所示:

{
    cmd:'NODE_HANDLE',
    type:'net.Server',
    msg:message
}

发送到IPC管道中的实际上是我们要发送的句柄文件描述符。这个message对象在写入到IPC管道时,也会通过JSON.stringfy()进行序列化。所以最终发送到IPC通道中的信息都是字符串,send()方法能发送消息和句柄并不意味着它能发送任何对象。

连接了IPC通道的子线程可以读取父进程发来的消息,将字符串通过JSON.parse()解析还原为对象后,才触发message事件将消息传递给应用层使用。在这个过程中,消息对象还要被进行过滤处理,message.cmd的值如果以NODE_为前缀,它将响应一个内部事件internalMessage,如果message.cmd值为NODE_HANDLE,它将取出message.type值和得到的文件描述符一起还原出一个对应的对象。

以发送的TCP服务器句柄为例,子进程收到消息后的还原过程代码如下:

function(message,handle,emit){
    var self = this;
    
    var server = new net.Server();
    server.listen(handler,function(){
      emit(server);
    });
}

这段还原代码,子进程根据message.type创建对应的TCP服务器对象,然后监听到文件描述符上。由于底层细节不被应用层感知,所以子进程中,开发者会有一种服务器对象就是从父进程中直接传递过来的错觉。

Node进程之间只有消息传递,不会真正的传递对象,这种错觉是抽象封装的结果。目前Node只支持我前面提到的几种句柄,并非任意类型的句柄都能在进程之间传递,除非它有完整的发送和还原的过程。

Node.js多进程架构模型

我们自己实现一个多进程架构守护Demo

16c565f2d5b5e5c2?w=533&h=352&f=png&s=47188
编写主进程

master.js 主要处理以下逻辑:

  • 创建一个 server 并监听 3000 端口。
  • 根据系统 cpus 开启多个子进程
  • 通过子进程对象的 send 方法发送消息到子进程进行通信
  • 在主进程中监听了子进程的变化,如果是自杀信号重新启动一个工作进程。
  • 主进程在监听到退出消息的时候,先退出子进程在退出主进程
// master.js
const fork = require('child_process').fork;
const cpus = require('os').cpus();

const server = require('net').createServer();
server.listen(3000);
process.title = 'node-master'

const workers = {};
const createWorker = () => {
    const worker = fork('worker.js')
    worker.on('message', function (message) {
        if (message.act === 'suicide') {
            createWorker();
        }
    })
    worker.on('exit', function(code, signal) {
        console.log('worker process exited, code: %s signal: %s', code, signal);
        delete workers[worker.pid];
    });
    worker.send('server', server);
    workers[worker.pid] = worker;
    console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);
}

for (let i=0; i<cpus.length; i++) {
    createWorker();
}

process.once('SIGINT', close.bind(this, 'SIGINT')); // kill(2) Ctrl-C
process.once('SIGQUIT', close.bind(this, 'SIGQUIT')); // kill(3) Ctrl-\
process.once('SIGTERM', close.bind(this, 'SIGTERM')); // kill(15) default
process.once('exit', close.bind(this));

function close (code) {
    console.log('进程退出!', code);

    if (code !== 0) {
        for (let pid in workers) {
            console.log('master process exited, kill worker pid: ', pid);
            workers[pid].kill('SIGINT');
        }
    }

    process.exit(0);
}

工作进程

worker.js 子进程处理逻辑如下:

  • 创建一个 server 对象,注意这里最开始并没有监听 3000 端口
  • 通过 message 事件接收主进程 send 方法发送的消息
  • 监听 uncaughtException 事件,捕获未处理的异常,发送自杀信息由主进程重建进程,子进程在链接关闭之后退出
// worker.js
const http = require('http');
const server = http.createServer((req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/plan'
    });
    res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid);
    throw new Error('worker process exception!'); // 测试异常进程退出、重启
});

let worker;
process.title = 'node-worker'
process.on('message', function (message, sendHandle) {
    if (message === 'server') {
        worker = sendHandle;
        worker.on('connection', function(socket) {
            server.emit('connection', socket);
        });
    }
});

process.on('uncaughtException', function (err) {
    console.log(err);
    process.send({act: 'suicide'});
    worker.close(function () {
        process.exit(1);
    })
})

Node.js 进程守护

什么是进程守护?

每次启动 Node.js 程序都需要在命令窗口输入命令 node app.js 才能启动,但如果把命令窗口关闭则Node.js 程序服务就会立刻断掉。除此之外,当我们这个 Node.js 服务意外崩溃了就不能自动重启进程了。这些现象都不是我们想要看到的,所以需要通过某些方式来守护这个开启的进程,执行 node app.js 开启一个服务进程之后,我还可以在这个终端上做些别的事情,且不会相互影响。,当出现问题可以自动重启。

如何实现进程守护

这里我只说一些第三方的进程守护框架,pm2 和 forever ,它们都可以实现进程守护,底层也都是通过上面讲的 child_process 模块和 cluster 模块 实现的,这里就不再提它们的原理。

pm2 指定生产环境启动一个名为 test 的 node 服务

pm2 start app.js --env production --name test

pm2常用api

  • pm2 stop Name/processID 停止某个服务,通过服务名称或者服务进程ID
  • pm2 delete Name/processID 删除某个服务,通过服务名称或者服务进程ID
  • pm2 logs [Name] 查看日志,如果添加服务名称,则指定查看某个服务的日志,不加则查看所有日志
  • pm2 start app.js -i 4 集群,-i <number of workers>参数用来告诉PM2以cluster_mode的形式运行你的app(对应的叫fork_mode),后面的数字表示要启动的工作线程的数量。如果给定的数字为0,PM2则会根据你CPU核心的数量来生成对应的工作线程。注意一般在生产环境使用cluster_mode模式,测试或者本地环境一般使用fork模式,方便测试到错误。
  • pm2 reload Name pm2 restart Name 应用程序代码有更新,可以用重载来加载新代码,也可以用重启来完成,reload可以做到0秒宕机加载新的代码,restart则是重新启动,生产环境中多用reload来完成代码更新!
  • pm2 show Name 查看服务详情
  • pm2 list 查看pm2中所有项目
  • pm2 monit用monit可以打开实时监视器去查看资源占用情况

pm2 官网地址:

http://pm2.keymetrics.io/docs...

forever 就不特殊说明了,官网地址

https://github.com/foreverjs/...

注意:二者更推荐pm2,看一下二者对比就知道我为什么更推荐使用pm2了。https://www.jianshu.com/p/fdc...

linux 关闭一个进程

  • 查找与进程相关的PID号

    ps aux | grep server

说明:

    root     20158  0.0  5.0 1251592 95396 ?       Sl   5月17   1:19 node /srv/mini-program-api/launch_pm2.js
上面是执行命令后在linux中显示的结果,第二个参数就是进程对应的PID


  • 杀死进程
  1. 以优雅的方式结束进程

    kill -l PID

    -l选项告诉kill命令用好像启动进程的用户已注销的方式结束进程。

当使用该选项时,kill命令也试图杀死所留下的子进程。
但这个命令也不是总能成功--或许仍然需要先手工杀死子进程,然后再杀死父进程。

  1. kill 命令用于终止进程

    例如: kill -9 [PID]

-9 表示强迫进程立即停止

这个强大和危险的命令迫使进程在运行时突然终止,进程在结束后不能自我清理。
危害是导致系统资源无法正常释放,一般不推荐使用,除非其他办法都无效。
当使用此命令时,一定要通过ps -ef确认没有剩下任何僵尸进程。
只能通过终止父进程来消除僵尸进程。如果僵尸进程被init收养,问题就比较严重了。
杀死init进程意味着关闭系统。
如果系统中有僵尸进程,并且其父进程是init,
而且僵尸进程占用了大量的系统资源,那么就需要在某个时候重启机器以清除进程表了。
  1. killall命令

    杀死同一进程组内的所有进程。其允许指定要终止的进程的名称,而非PID。

    killall httpd

Node.js 线程

Node.js关于单线程的误区

const http = require('http');

const server = http.createServer();
server.listen(3000,()=>{
    process.title='程序员成长指北测试进程';
    console.log('进程id',process.pid)
})

仍然看本文第一段代码,创建了http服务,开启了一个进程,都说了Node.js是单线程,所以 Node 启动后线程数应该为 1,但是为什么会开启7个线程呢?难道Javascript不是单线程不知道小伙伴们有没有这个疑问?

解释一下这个原因:

Node 中最核心的是 v8 引擎,在 Node 启动后,会创建 v8 的实例,这个实例是多线程的。

  • 主线程:编译、执行代码。
  • 编译/优化线程:在主线程执行的时候,可以优化代码。
  • 分析器线程:记录分析代码运行时间,为 Crankshaft 优化代码执行提供依据。
  • 垃圾回收的几个线程。

所以大家常说的 Node 是单线程的指的是 JavaScript 的执行是单线程的(开发者编写的代码运行在单线程环境中),但 Javascript 的宿主环境,无论是 Node 还是浏览器都是多线程的因为libuv中有线程池的概念存在的,libuv会通过类似线程池的实现来模拟不同操作系统的异步调用,这对开发者来说是不可见的。

某些异步 IO 会占用额外的线程

还是上面那个例子,我们在定时器执行的同时,去读一个文件:

const fs = require('fs')
setInterval(() => {
    console.log(new Date().getTime())
}, 3000)

fs.readFile('./index.html', () => {})

线程数量变成了 11 个,这是因为在 Node 中有一些 IO 操作(DNS,FS)和一些 CPU 密集计算(Zlib,Crypto)会启用 Node 的线程池,而线程池默认大小为 4,因为线程数变成了 11。
我们可以手动更改线程池默认大小:

process.env.UV_THREADPOOL_SIZE = 64

一行代码轻松把线程变成 71。

Libuv

Libuv 是一个跨平台的异步IO库,它结合了UNIX下的libev和Windows下的IOCP的特性,最早由Node的作者开发,专门为Node提供多平台下的异步IO支持。Libuv本身是由C++语言实现的,Node中的非苏塞IO以及事件循环的底层机制都是由libuv实现的。

libuv架构图

16c565ec3aaa0424?w=323&h=156&f=jpeg&s=7864

在Window环境下,libuv直接使用Windows的IOCP来实现异步IO。在非Windows环境下,libuv使用多线程来模拟异步IO。

注意下面我要说的话,Node的异步调用是由libuv来支持的,以上面的读取文件的例子,读文件实质的系统调用是由libuv来完成的,Node只是负责调用libuv的接口,等数据返回后再执行对应的回调方法。

Node.js 线程创建

直到 Node 10.5.0 的发布,官方才给出了一个实验性质的模块 worker_threads 给 Node 提供真正的多线程能力。

先看下简单的 demo:

const {
  isMainThread,
  parentPort,
  workerData,
  threadId,
  MessageChannel,
  MessagePort,
  Worker
} = require('worker_threads');

function mainThread() {
  for (let i = 0; i < 5; i++) {
    const worker = new Worker(__filename, { workerData: i });
    worker.on('exit', code => { console.log(`main: worker stopped with exit code ${code}`); });
    worker.on('message', msg => {
      console.log(`main: receive ${msg}`);
      worker.postMessage(msg + 1);
    });
  }
}

function workerThread() {
  console.log(`worker: workerDate ${workerData}`);
  parentPort.on('message', msg => {
    console.log(`worker: receive ${msg}`);
  }),
  parentPort.postMessage(workerData);
}

if (isMainThread) {
  mainThread();
} else {
  workerThread();
}

上述代码在主线程中开启五个子线程,并且主线程向子线程发送简单的消息。

由于 worker_thread 目前仍然处于实验阶段,所以启动时需要增加 --experimental-worker flag,运行后观察活动监视器,开启了5个子线程

16c6cfb939b5b268?w=1306&h=238&f=jpeg&s=49232

worker_thread 模块

worker_thread 核心代码(地址https://github.com/nodejs/nod...
worker_thread 模块中有 4 个对象和 2 个类,可以自己去看上面的源码。

  • isMainThread: 是否是主线程,源码中是通过 threadId === 0 进行判断的。
  • MessagePort: 用于线程之间的通信,继承自 EventEmitter。
  • MessageChannel: 用于创建异步、双向通信的通道实例。
  • threadId: 线程 ID。
  • Worker: 用于在主线程中创建子线程。第一个参数为 filename,表示子线程执行的入口。
  • parentPort: 在 worker 线程里是表示父进程的 MessagePort 类型的对象,在主线程里为 null
  • workerData: 用于在主进程中向子进程传递数据(data 副本)

总结

多进程 vs 多线程

对比一下多线程与多进程:

属性多进程多线程比较
数据数据共享复杂,需要用IPC;数据是分开的,同步简单因为共享进程数据,数据共享简单,同步复杂各有千秋
CPU、内存占用内存多,切换复杂,CPU利用率低占用内存少,切换简单,CPU利用率高多线程更好
销毁、切换创建销毁、切换复杂,速度慢创建销毁、切换简单,速度很快多线程更好
coding编码简单、调试方便编码、调试复杂编码、调试复杂
可靠性进程独立运行,不会相互影响线程同呼吸共命运多进程更好
分布式可用于多机多核分布式,易于扩展只能用于多核分布式多进程更好

加入我们一起学习吧!

16b8a3d23a52b7d0?w=940&h=400&f=jpeg&s=217901
node学习交流群

交流群满100人不能自动进群, 请添加群助手微信号:【coder_qi】备注node,自动拉你入群。

查看原文

赞 200 收藏 150 评论 7

路游远方 赞了文章 · 2019-08-17

美化文件上传按钮自定义input file样式

input file的样式不能直接用css来美化,我们可以曲线救国,把input file的透明度降低为0,相当于把这个控件隐藏了,实际上只是透明度为0,还是存在的,然后把div套上去,让div充当file的按钮。

clipboard.png

所以这个办法很简单,但是,把input file的透明度降低,连选择文件后的文件名也被隐藏了,这下可怎么办?那就只能用jquery获取file的文件名了。

<!DOCTYPE html>
<html>
<head>
    <title>文件上传file美化</title>
    <style type="text/css">
    *{margin:0;padding: 0;}
        #btn{
            width: 120px;
            height: 45px;
            background: #39f;
            line-height: 45px;
            text-align: center;
            color: #fff;
        }

        #btn .file{
            opacity: 0;
            position: relative;
            top: -56px;
            width: 100%;
            height: 45px;
        }
    </style>
</head>
<body>

<!-- 用一个div来当作美化的上传按钮,file按钮被透明化 -->
<div id="btn">
文件上传
<input type="file" class="file" onclick="daojishi()">
</div>

<!-- 文件名显示区域 -->
<div id="filename"></div>

<script data-original="https://www.jq22.com/jquery/jquery-3.3.1.js"></script>
<!-- 轮询文件名 -->
<script>
    function daojishi() {
        setInterval("getname()",1000);
    }
</script>

<!-- 获取文件名 -->
<script>
    function getname() {
        var filename = $("#btn .file").val();
        $("#filename").text(filename);
    }
</script>

</body>
</html>

搞定!

clipboard.png

作者:TANKING
网站:http://www.likeyunba.com

查看原文

赞 4 收藏 2 评论 1

路游远方 赞了文章 · 2019-08-17

提高效率的 10 个 Git 小技巧

1. 从某个分支检出单个文件

你是否有过破坏了某个文件,想要重新开始?
或者需要的文件在另一个分支中?
下面的命令让你直接从某个分支取到那个文件。

git checkout some-other-branch -- yarn.lock

你也可以从某个 commit 中取到 yarn.lock 文件

git checkout 9146467 -- yarn.lock

1-checkout.png

相比 cherry-pick 拿到某个 commit 的所有文件,这个技巧可以只拿到想要的那一个文件。

2. 查看日志时过滤掉 merge commits

使用 merge 合并时会产生一个新的提交,有时候这个提交很烦人。如果你想要查看日志时过滤掉这些合并的提交,可以使用以下命令:

git log --oneline --no-merges

2-filter-merge.png

3. 重写最后一次的 commit message

如果你最近的一次提交中,commit 信息写的不好或者有错别字,可以用以下命令修改:

git commit -v --amend

这里的 -v 是可选的,它可以提过一些额外信息来帮助你描述 commit message

4. 清除所有未跟踪的变更

首先来解释一个概念:
如果你创建了一个新文件,这个文件之前不存在于 git 历史中,那么这个文件就是一个未跟踪的变更。为了跟踪这个文件,你需要将它提交到 git 中。

3-clear-untracked.png

如果使用 git checkout . 则会清除所有已经跟踪的变更。而使用以下命令可以清除所有未跟踪的变更:

git clean -f -d

5. 打印一个可视化的 log

使用以下命令可以打印出可视化的 log

git log --pretty=oneline --graph --decorate --all

(译者注:凑合看,跟 sourcetree 的还是不能比~)

6. 向 Git 查询 changelog

此命令可以向 Git 查询在两个 commit 之间,都有谁做了哪些变更,看起来就像一个 changelog 一样

git shortlog <commit>..HEAD

上面的 <commit> 填写 commit 的 hash 值,就可以查出该 commit 和 HEAD 之间的变化,其中 .. 后面的 HEAD 也可以省略

4-query-changelog.png

你也可以用 git shortlog HEAD~20.. 取最近 20 个 commit 的记录

7. 查询指定日期的 log

你可能需要查询某两天之间的 git log,这时可以用 git log 命令配合 --since--util 标识符

假如你想要查询 2016年2月10日 至 2016年2月19日 之间的日志,你可以运行:

git log --since='FEB 10 2016' --until='FEB 19 2016'

8. 列出所有 git aliases

有时候你可能忘记了之前设置的 git 别名,下面的命令虽不是 git 的功能,但是却能帮你找到所有的 git 别名

git config -l | grep alias | sed 's/^alias\.//g'

9. 查询包含某个关键字的 commits

如果你知道要找的代码具体写的是什么,或者知道某个特别的关键字,你就可以用它来搜索。

git log -S"config.menu_items"

本例中会查找所有包含 config.menu_items 的提交

10. 终极技能

git help -g

可以看到类似下面的 git 教程列表,通过 git help <concept> 即可在浏览器打开指定的教程网页,左边那一列即为 <concept> 名称

The common Git guides are:

   attributes   定义 Git 路径的属性
   everyday    每天学点有用的 Git 命令
   glossary     一个 Git 词汇表
   ignore        指定 Git 忽略文件
   modules     定义 Git 子模块
   revisions     指定 Git 的修订版和范围
   tutorial       Git 的教程介绍 (for version 1.5.1 or newer)
   workflows   一个推荐的 Git 工作流概述

博客原文链接

欢迎关注我的公众号:码力全开(codingonfire)

codingonfire.jpg

查看原文

赞 36 收藏 25 评论 0

路游远方 赞了文章 · 2019-08-17

Vue项目优化

在日常的Vue项目开发中,随着业务的日渐复杂,代码量的日益增加,随之带来的问题就是打包后的vendor.js体积过大,导致加载时空白页时间过长,给用户的体验太差。为此我们需要减少vendor.js的体积,从本质上来解决这种问题。

这里大概例举几个在项目中实际用到过的方法,优化后的js大概缩小了50%左右,效果还是挺明显的。

1,webpack externals

为什么要配置externals

官方解释:webpack 中的 externals 配置提供了不从 bundle 中引用依赖的方式。解决的是,所创建的 bundle 依赖于那些存在于用户环境(consumer environment)中的依赖。

比如我们会在项目中引用各种第三方的js文件,如 jquery,vue,axios 等,如果我们在打包时排除这些依赖,则可以大大减小文件的大小,提高页面加载速度,配置方法如下:

// 在 index.js 中引入资源
<script data-original="jquery.js"></script>
<script data-original="vuex.js"></script>


module.exports = {
     ...
     output: {
       ...
     },
     externals : {
       jquery: 'jQuery'
       'vue': 'Vue',
     }
     ...
   }

这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:

import $ from 'jquery';
import Vue from 'vue'

这样不仅之前对第三方库的用法方式不变,还把第三方库剥离出webpack的打包中,从而加速webpack的打包速度。

2,组件按需加载

这也是可以优化的点。如果频繁使用了第三方组件/UI库,如我的项目中经常使用了 element-ui 组件库,如果全部引入,项目体积非常大,这时可以按需引入组件。

首先,我们需要安装 babel-plugin-component

npm install babel-plugin-component -D

然后,将.babelrc 修改为:

{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

然后引入部分组件,这样一来,就不需要引入样式了,插件会帮我们处理。

// main.js
import Vue from 'vue'
import { Dialog, Loading } from 'element-ui'

[Dialog, Loading].forEach((comp) => {
    Vue.use(comp);
})

3,路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

方法一

const login = () => import('@/components/login')
const router = new VueRouter({
  routes: [
    { path: '/login', component: login }
  ]
})
注意:这个时候我们需要安装 babel-plugin-syntax-dynamic-import 插件,这样打包时才会识别该语法

方法二

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

4,数据缓存——keep-alive

VUE2.0中提供了一个keep-alive方法,可以用来缓存组件,避免多次加载相应的组件,减少性能的消耗。

举例:用户浏览A页面,待A页面的资源都加载完毕了,用户通过路由跳转到了B页面,之后又退回到了A页面,如果没有特殊处理的话,A页面的资源会重新加载一遍,造成资源浪费;若我们做了缓存,则可以减少请求的次数,极高用户体验。

1,缓存部分页面

<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>

// router 设置如下
routers:[
    {   path: '/home',
        name: home,
        meta: {keepAlive: true}      //  设置为true表示需要缓存,不设置或者false表示不需要缓存              
    }
]

2,缓存组件

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>

// include 和 exclude 属性允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

除了以上的优化方法外,还有常规的 图片的压缩合并、CDN加速、压缩代码等方法,大家应该都很熟悉了,这里就不在一一阐述了。如果大家还有好的优化方案,欢迎大家留言交流~

查看原文

赞 113 收藏 83 评论 8

路游远方 赞了文章 · 2019-08-17

Vue项目优化

在日常的Vue项目开发中,随着业务的日渐复杂,代码量的日益增加,随之带来的问题就是打包后的vendor.js体积过大,导致加载时空白页时间过长,给用户的体验太差。为此我们需要减少vendor.js的体积,从本质上来解决这种问题。

这里大概例举几个在项目中实际用到过的方法,优化后的js大概缩小了50%左右,效果还是挺明显的。

1,webpack externals

为什么要配置externals

官方解释:webpack 中的 externals 配置提供了不从 bundle 中引用依赖的方式。解决的是,所创建的 bundle 依赖于那些存在于用户环境(consumer environment)中的依赖。

比如我们会在项目中引用各种第三方的js文件,如 jquery,vue,axios 等,如果我们在打包时排除这些依赖,则可以大大减小文件的大小,提高页面加载速度,配置方法如下:

// 在 index.js 中引入资源
<script data-original="jquery.js"></script>
<script data-original="vuex.js"></script>


module.exports = {
     ...
     output: {
       ...
     },
     externals : {
       jquery: 'jQuery'
       'vue': 'Vue',
     }
     ...
   }

这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:

import $ from 'jquery';
import Vue from 'vue'

这样不仅之前对第三方库的用法方式不变,还把第三方库剥离出webpack的打包中,从而加速webpack的打包速度。

2,组件按需加载

这也是可以优化的点。如果频繁使用了第三方组件/UI库,如我的项目中经常使用了 element-ui 组件库,如果全部引入,项目体积非常大,这时可以按需引入组件。

首先,我们需要安装 babel-plugin-component

npm install babel-plugin-component -D

然后,将.babelrc 修改为:

{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

然后引入部分组件,这样一来,就不需要引入样式了,插件会帮我们处理。

// main.js
import Vue from 'vue'
import { Dialog, Loading } from 'element-ui'

[Dialog, Loading].forEach((comp) => {
    Vue.use(comp);
})

3,路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

方法一

const login = () => import('@/components/login')
const router = new VueRouter({
  routes: [
    { path: '/login', component: login }
  ]
})
注意:这个时候我们需要安装 babel-plugin-syntax-dynamic-import 插件,这样打包时才会识别该语法

方法二

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

4,数据缓存——keep-alive

VUE2.0中提供了一个keep-alive方法,可以用来缓存组件,避免多次加载相应的组件,减少性能的消耗。

举例:用户浏览A页面,待A页面的资源都加载完毕了,用户通过路由跳转到了B页面,之后又退回到了A页面,如果没有特殊处理的话,A页面的资源会重新加载一遍,造成资源浪费;若我们做了缓存,则可以减少请求的次数,极高用户体验。

1,缓存部分页面

<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>

// router 设置如下
routers:[
    {   path: '/home',
        name: home,
        meta: {keepAlive: true}      //  设置为true表示需要缓存,不设置或者false表示不需要缓存              
    }
]

2,缓存组件

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>

// include 和 exclude 属性允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

除了以上的优化方法外,还有常规的 图片的压缩合并、CDN加速、压缩代码等方法,大家应该都很熟悉了,这里就不在一一阐述了。如果大家还有好的优化方案,欢迎大家留言交流~

查看原文

赞 113 收藏 83 评论 8

路游远方 赞了文章 · 2019-08-17

Vue项目优化

在日常的Vue项目开发中,随着业务的日渐复杂,代码量的日益增加,随之带来的问题就是打包后的vendor.js体积过大,导致加载时空白页时间过长,给用户的体验太差。为此我们需要减少vendor.js的体积,从本质上来解决这种问题。

这里大概例举几个在项目中实际用到过的方法,优化后的js大概缩小了50%左右,效果还是挺明显的。

1,webpack externals

为什么要配置externals

官方解释:webpack 中的 externals 配置提供了不从 bundle 中引用依赖的方式。解决的是,所创建的 bundle 依赖于那些存在于用户环境(consumer environment)中的依赖。

比如我们会在项目中引用各种第三方的js文件,如 jquery,vue,axios 等,如果我们在打包时排除这些依赖,则可以大大减小文件的大小,提高页面加载速度,配置方法如下:

// 在 index.js 中引入资源
<script data-original="jquery.js"></script>
<script data-original="vuex.js"></script>


module.exports = {
     ...
     output: {
       ...
     },
     externals : {
       jquery: 'jQuery'
       'vue': 'Vue',
     }
     ...
   }

这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:

import $ from 'jquery';
import Vue from 'vue'

这样不仅之前对第三方库的用法方式不变,还把第三方库剥离出webpack的打包中,从而加速webpack的打包速度。

2,组件按需加载

这也是可以优化的点。如果频繁使用了第三方组件/UI库,如我的项目中经常使用了 element-ui 组件库,如果全部引入,项目体积非常大,这时可以按需引入组件。

首先,我们需要安装 babel-plugin-component

npm install babel-plugin-component -D

然后,将.babelrc 修改为:

{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

然后引入部分组件,这样一来,就不需要引入样式了,插件会帮我们处理。

// main.js
import Vue from 'vue'
import { Dialog, Loading } from 'element-ui'

[Dialog, Loading].forEach((comp) => {
    Vue.use(comp);
})

3,路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

方法一

const login = () => import('@/components/login')
const router = new VueRouter({
  routes: [
    { path: '/login', component: login }
  ]
})
注意:这个时候我们需要安装 babel-plugin-syntax-dynamic-import 插件,这样打包时才会识别该语法

方法二

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

4,数据缓存——keep-alive

VUE2.0中提供了一个keep-alive方法,可以用来缓存组件,避免多次加载相应的组件,减少性能的消耗。

举例:用户浏览A页面,待A页面的资源都加载完毕了,用户通过路由跳转到了B页面,之后又退回到了A页面,如果没有特殊处理的话,A页面的资源会重新加载一遍,造成资源浪费;若我们做了缓存,则可以减少请求的次数,极高用户体验。

1,缓存部分页面

<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>

// router 设置如下
routers:[
    {   path: '/home',
        name: home,
        meta: {keepAlive: true}      //  设置为true表示需要缓存,不设置或者false表示不需要缓存              
    }
]

2,缓存组件

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>

// include 和 exclude 属性允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

除了以上的优化方法外,还有常规的 图片的压缩合并、CDN加速、压缩代码等方法,大家应该都很熟悉了,这里就不在一一阐述了。如果大家还有好的优化方案,欢迎大家留言交流~

查看原文

赞 113 收藏 83 评论 8

路游远方 收藏了文章 · 2019-07-11

JavaScript中的浅拷贝和深拷贝

在JavaScript中,对于ObjectArray这类引用类型值,当从一个变量向另一个变量复制引用类型值时,这个值的副本其实是一个指针,两个变量指向同一个堆对象,改变其中一个变量,另一个也会受到影响。

这种拷贝分为两种情况:拷贝引用和拷贝实例,也就是我们说的浅拷贝和深拷贝

浅拷贝(shallow copy)

拷贝原对象的引用,这是最简单的浅拷贝。

// 对象
var o1 = {a: 1};
var o2 = o1;

console.log(o1 === o2);  // =>true
o2.a = 2; 
console.log(o1.a); // => 2

// 数组
var o1 = [1,2,3];
var o2 = o1;

console.log(o1 === o2); // => true
o2.push(4);
console.log(o1); // => [1,2,3,4]

拷贝原对象的实例,但是对其内部的引用类型值,拷贝的是其引用,常用的就是如jquey中的$.extend({}, obj);Array.prototype.slice()Array.prototype.concat()都会返回一个数组或者对象的浅拷贝,举个例子:

var o1 = ['darko', {age: 22}];
var o2 = o1.slice(); // 根据Array.prototype.slice()的特性,这里会返回一个o1的浅拷贝对象

console.log(o1 === o2); // => false,说明o2拷贝的是o1的一个实例

o2[0] = 'lee';
console.log(o1[0]); // => "darko" o1和o2内部包含的基本类型值,复制的是其实例,不会相互影响

o2[1].age = 23;
console.log(o1[1].age); // =>23 o1和o2内部包含的引用类型值,复制的是其引用,会相互影响

可以通过Array.prototype.slice()jQuery中的$.extend({}, obj)完成对一个数组或者对象的浅拷贝,我们也可以自己写一个简单浅拷贝函数来加深对浅拷贝的理解、

// 浅拷贝实现,仅供参考
function shallowClone(source) {
    if (!source || typeof source !== 'object') {
        throw new Error('error arguments');
    }
    var targetObj = source.constructor === Array ? [] : {};
    for (var keys in source) {
        if (source.hasOwnProperty(keys)) {
            targetObj[keys] = source[keys];
        }
    }
    return targetObj;
}

深拷贝(deep copy)

深拷贝也就是拷贝出一个新的实例,新的实例和之前的实例互不影响,深拷贝的实现有几种方法,首先我们可以借助jQuery,lodash等第三方库完成一个深拷贝实例。在jQuery中可以通过添加一个参数来实现递归extend,调用$.extend(true, {}, ...)就可以实现一个深拷贝。

我们也可以自己实现一个深拷贝的函数,通常有两种方式,一种就是用递归的方式来做,还有一种是利用JSON.stringifyJSON.parse来做,这两种方式各有优劣,先来看看递归的方法怎么做。

jQuery中的extend方法基本的就是按照这个思路实现的,但是没有办法处理源对象内部循环引用的问题,同时对Date,Funcion等类型值也没有实现真正的深度复制,但是这些类型的值在重新定义的时候一般都是直接覆盖,所以也不会对源对象产生影响,从一定程度上来说也算是实现了一个深拷贝。

// 递归实现一个深拷贝
function deepClone(source){
   if(!source || typeof source !== 'object'){
     throw new Error('error arguments', 'shallowClone');
   }
   var targetObj = source.constructor === Array ? [] : {};
   for(var keys in source){
      if(source.hasOwnProperty(keys)){
         if(source[keys] && typeof source[keys] === 'object'){
           targetObj[keys] = source[keys].constructor === Array ? [] : {};
           targetObj[keys] = deepClone(source[keys]);
         }else{
           targetObj[keys] = source[keys];
         }
      } 
   }
   return targetObj;
}
// test example
var o1 = {
  arr: [1, 2, 3],
  obj: {
    key: 'value'
  },
  func: function(){
    return 1;
  }
};
var o3 = deepClone(o1);
console.log(o3 === o1); // => false
console.log(o3.obj === o1.obj); // => false
console.log(o2.func === o1.func); // => true
  

还有一种实现深拷贝的方式是利用JSON对象中的parsestringify,JOSN对象中的stringify可以把一个js对象序列化为一个JSON字符串,parse可以把JSON字符串反序列化为一个js对象,通过这两个方法,也可以实现对象的深复制。

我们从下面的例子就可以看到,源对象的方法在拷贝的过程中丢失了,这是因为在序列化JavaScript对象时,所有函数和原型成员会被有意忽略,这个实现可以满足一些比较简单的情况,能够处理JSON格式所能表示的所有数据类型,同时如果在对象中存在循环应用的情况也无法正确处理。

// 利用JSON序列化实现一个深拷贝
function deepClone(source){
  return JSON.parse(JSON.stringify(source));
}
var o1 = {
  arr: [1, 2, 3],
  obj: {
    key: 'value'
  },
  func: function(){
    return 1;
  }
};
var o2 = deepClone(o1);
console.log(o2); // => {arr: [1,2,3], obj: {key: 'value'}}
查看原文

路游远方 收藏了文章 · 2019-07-09

详解vue生命周期

首先,每个Vue实例在被创建之前都要经过一系列的初始化过程,这个过程就是vue的生命周期。首先看一张图吧~这是官方文档上的图片相信大家一定都会很熟悉:

clipboard.png

可以看到在vue一整个的生命周期中会有很多钩子函数提供给我们在vue生命周期不同的时刻进行操作, 那么先列出所有的钩子函数,然后我们再一一详解:

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestroy
  • destroyed

先来一波代码,各位复制在浏览器中运行,打开console查看就行了:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>vue生命周期学习</title>
  <script data-original="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
</head>
<body>
  <div id="app">
    <h1>{{message}}</h1>
  </div>
</body>
<script>
  var vm = new Vue({
    el: '#app',
    data: {
      message: 'Vue的生命周期'
    },
    beforeCreate: function() {
      console.group('------beforeCreate创建前状态------');
      console.log("%c%s", "color:red" , "el     : " + this.$el); //undefined
      console.log("%c%s", "color:red","data   : " + this.$data); //undefined 
      console.log("%c%s", "color:red","message: " + this.message) 
    },
    created: function() {
      console.group('------created创建完毕状态------');
      console.log("%c%s", "color:red","el     : " + this.$el); //undefined
      console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化 
      console.log("%c%s", "color:red","message: " + this.message); //已被初始化
    },
    beforeMount: function() {
      console.group('------beforeMount挂载前状态------');
      console.log("%c%s", "color:red","el     : " + (this.$el)); //已被初始化
      console.log(this.$el);
      console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化  
      console.log("%c%s", "color:red","message: " + this.message); //已被初始化  
    },
    mounted: function() {
      console.group('------mounted 挂载结束状态------');
      console.log("%c%s", "color:red","el     : " + this.$el); //已被初始化
      console.log(this.$el);    
      console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化
      console.log("%c%s", "color:red","message: " + this.message); //已被初始化 
    },
    beforeUpdate: function () {
      console.group('beforeUpdate 更新前状态===============》');
      console.log("%c%s", "color:red","el     : " + this.$el);
      console.log(this.$el);   
      console.log("%c%s", "color:red","data   : " + this.$data); 
      console.log("%c%s", "color:red","message: " + this.message); 
    },
    updated: function () {
      console.group('updated 更新完成状态===============》');
      console.log("%c%s", "color:red","el     : " + this.$el);
      console.log(this.$el); 
      console.log("%c%s", "color:red","data   : " + this.$data); 
      console.log("%c%s", "color:red","message: " + this.message); 
    },
    beforeDestroy: function () {
      console.group('beforeDestroy 销毁前状态===============》');
      console.log("%c%s", "color:red","el     : " + this.$el);
      console.log(this.$el);    
      console.log("%c%s", "color:red","data   : " + this.$data); 
      console.log("%c%s", "color:red","message: " + this.message); 
    },
    destroyed: function () {
      console.group('destroyed 销毁完成状态===============》');
      console.log("%c%s", "color:red","el     : " + this.$el);
      console.log(this.$el);  
      console.log("%c%s", "color:red","data   : " + this.$data); 
      console.log("%c%s", "color:red","message: " + this.message)
    }
  })
</script>
</html>

运行后打开console可以看到打印出来内容如下:

clipboard.png

可以看到一个vue实例在创建过程中调用的几个生命周期钩子。

1. 在beforeCreate和created钩子函数之间的生命周期

在这个生命周期之间,进行初始化事件,进行数据的观测,可以看到在created的时候数据已经和data属性进行绑定(放在data中的属性当值发生改变的同时,视图也会改变)。
注意看下:此时还是没有el选项

2. created钩子函数和beforeMount间的生命周期

clipboard.png

在这一阶段发生的事情还是比较多的。

首先会判断对象是否有el选项如果有的话就继续向下编译,如果没有el选项,则停止编译,也就意味着停止了生命周期,直到在该vue实例上调用vm.$mount(el)。此时注释掉代码中:

el: '#app',

然后运行可以看到到created的时候就停止了:

clipboard.png

如果我们在后面继续调用vm.$mount(el),可以发现代码继续向下执行了

vm.$mount(el) //这个el参数就是挂在的dom接点

clipboard.png

然后,我们往下看,template参数选项的有无对生命周期的影响。
(1).如果vue实例对象中有template参数选项,则将其作为模板编译成render函数。
(2).如果没有template选项,则将外部HTML作为模板编译。
(3).可以看到template中的模板优先级要高于outer HTML的优先级。
修改代码如下, 在HTML结构中增加了一串html,在vue对象中增加了template选项

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>vue生命周期学习</title>
  <script data-original="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
</head>
<body>
  <div id="app">
    <!--html中修改的-->
    <h1>{{message + '这是在outer HTML中的'}}</h1>
  </div>
</body>
<script>
  var vm = new Vue({
    el: '#app',
    template: "<h1>{{message +'这是在template中的'}}</h1>", //在vue配置项中修改的
    data: {
      message: 'Vue的生命周期'
    }
</script>
</html>

执行后的结果可以看到在页面中显示的是:

clipboard.png

那么将vue对象中template的选项注释掉后打印如下信息:

clipboard.png

这下就可以想想什么el的判断要在template之前了~是因为vue需要通过el找到对应的outer template。

在vue对象中还有一个render函数,它是以createElement作为参数,然后做渲染操作,而且我们可以直接嵌入JSX.

new Vue({
    el: '#app',
    render: function(createElement) {
        return createElement('h1', 'this is createElement')
    }
})

可以看到页面中渲染的是:

clipboard.png

所以综合排名优先级:
render函数选项 > template选项 > outer HTML.

3. beforeMount和mounted 钩子函数间的生命周期

clipboard.png

可以看到此时是给vue实例对象添加$el成员,并且替换掉挂在的DOM元素。因为在之前console中打印的结果可以看到beforeMount之前el上还是undefined。

4. mounted

注意看下面截图:

clipboard.png

在mounted之前h1中还是通过{{message}}进行占位的,因为此时还有挂在到页面上,还是JavaScript中的虚拟DOM形式存在的。在mounted之后可以看到h1中的内容发生了变化。

5. beforeUpdate钩子函数和updated钩子函数间的生命周期

clipboard.png

当vue发现data中的数据发生了改变,会触发对应组件的重新渲染,先后调用beforeUpdateupdated钩子函数。我们在console中输入:

vm.message = '触发组件更新'

发现触发了组件的更新:

clipboard.png

6.beforeDestroy和destroyed钩子函数间的生命周期

clipboard.png

beforeDestroy钩子函数在实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed钩子函数在Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

本文是个人对vue的生命周期的理解,有什么不对的地方还请大牛多多指点~

查看原文

认证与成就

  • 获得 2 次点赞
  • 获得 13 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 12 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-08-29
个人主页被 280 人浏览