认识CPU

CPU是一个系统的核心所在,它推动了所有软件的运行。

CPU的核

一般我们会说,这是一个4核CPU,或者是一个4核8线程的CPU,这是什么意思呢?

物理CPU

指机器上的插槽插的CPU个数。物理CPU的数量,可以通过查询系统中不重复的physical id数量来判断:

1
2
$ cat /proc/cpuinfo |grep "physical id"|sort |uniq|wc -l
1

明显看到我的机器上只有一个物理CPU,主板上插了一个CPU。

每个CPU都会集成一个或者多个处理器芯片(称为Core,核心)。

CPU最初发展的时候是一个CPU一个处理核心,CPU的性能主要靠提高核心工作频率来提高,但是仅仅提高单核芯片的速度会产生过多热量且无法带来相应的性能改善。

为了提升处理器的能效,于是发展出来了双核心CPU(Dual-core processor)和多核心的CPU(Multi-core processor),在物理上是把2个或者更多的独立处理器芯片封装在一个单一的集成电路中。

在操作系统中,可以看到具有相同physical id的CPU是同一个物理CPU封装的线程或核心(下面会讲到线程)。

总物理核数 = 物理CPU个数 X 每颗物理CPU的核数

查看CPU核数:

1
2
$ cat /proc/cpuinfo| grep "cpu cores"| uniq
cpu cores : 4

以上是我的电脑的CPU核心数,有4核物理核的CPU。

逻辑CPU

开始的时候CPU是一个核心一个线程,为了进一步提升CPU的处理能力,Intel又引入了HT(Hyper-Threading,超线程)的技术,一个Core打开HT之后,在操作系统看来就是两个核,当然这个核是逻辑上的概念,所以也被称为逻辑处理器(Logical Processor)。

**“超线程”(Hyperthreading Technology)**技术就是通过采用特殊的硬件指令,可以把两个逻辑内核模拟成两个物理超线程芯片,在单处理器中实现线程级的并行计算,同时在相应的软硬件的支持下大幅度的提高运行效能,从而实现在单处理器上模拟双处理器的效能。其实,从实质上说,超线程是一种可以将CPU内部暂时闲置处理资源充分“调动”起来的技术。

所以,逻辑CPU的数量有时会大于物理CPU的数量,是因为开了超线程技术,计算公式如下:

总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数

在操作系统中查看逻辑CPU的个数:

1
2
$ cat /proc/cpuinfo| grep "processor"| wc -l
8

查看/proc/cpuinfo可以看到,分别各有2个processorcore id是一样的,也即这2个逻辑CPU是同一个CPU核心的超线程。

查询CPU是否启用超线程:

1
2
3
$ cat /proc/cpuinfo | grep -e "cpu cores" -e "siblings" | sort | uniq
cpu cores : 4
siblings : 8

  • cpu cores指的是一个物理CPU有几个核
  • siblings指的是一个物理CPU有几个逻辑CPU

如果cpu cores数量和siblings数量一致,则没有启用超线程,如果siblings是cpu cores的两倍,则说明支持超线程,并且超线程已打开。

要实现HT的功能,除了CPU要支持外,还需要主板芯片组,主板BIOS,以及操作系统的支持。一般说来,最大发挥HT技术的运行效能还需要真正支持超线程技术的软件。

虽然采用超线程技术能同时执行两个线程,但它并不象两个真正的CPU那样,每个CPU都具有独立的资源。当两个线程都同时需要某一个资源时,其中一个要暂时停止,并让出资源,直到这些资源闲置后才能继续。因此超线程的性能并不等于两颗CPU的性能。

根据Intel提供的数据,这样一个技术会使得设备面积增大5%,但是性能提高15%~30%。

来一张我的机器的CPU架构图:

CPU指令

CPU指令集

为什么CPU能控制一个庞大而复杂的电脑系统?这就关乎到指令集。

CPU依靠指令来计算和控制系统,对电脑下达的每一个命令都需要CPU根据预先设定好的某一条指令来完成。这些预先设定好的指令是预存在CPU中的。CPU依靠外来指令“激活”自己内存的指令,来计算和操控电脑。

每款CPU在设计时就规定了一系列与其硬件电路相配合的指令系统,这就是所谓的指令集

指令周期

说到指令,就不能不提指令周期。

指令周期是执行一条指令所需要的时间,是从取指令、分析指令到执行完所需的全部时间。它一般由若干个机器周期组成。

机器周期(也称为CPU周期)指计算机完成一个基本操作所需要的时间。例如,取指令、存储器读、存储器写等,这每一项工作称为一个基本操作。

机器周期还不是计算机最小最基本的时间单位,计算机中最基本的、最小的时间单位是时钟周期,在一个时钟周期内,CPU仅完成一个最基本的动作。

计算机之所以能自动地工作,是因为CPU能从存放程序的内存里取出一条指令并执行这条指令;紧接着又是取指令,执行指令,如此周而复始,构成了一个封闭的循环。除非遇到停机指令,否则这个循环将一直继续下去。

CPU工作频率

时钟频率

时钟频率(又译:时钟频率速度,clock rate),是指同步电路中时钟的基础频率,它以“若干次周期每秒”来度量,量度单位采用SI单位赫兹(Hz)。它是评定CPU性能的重要指标。

在电子技术中,脉冲信号是一个按一定电压幅度,一定时间间隔连续发出的脉冲信号。脉冲信号之间的时间间隔称为周期;而将在单位时间(如1秒)内所产生的脉冲个数称为频率

时钟频率的单位有:Hz(赫兹)、kHz(千赫兹)、MHz(兆赫兹)、GHz【吉赫兹(1吉=1000000000)】。其中1GHz=1000MHz,1MHz=1000kHz,1kHz=1000Hz。

例如,一个5GHz的CPU每秒运行50亿个时钟周期。

主频

主频用来表示CPU的运算、处理数据的速度,单位是兆赫(MHz)或千兆赫(GHz)。通常,主频越高,CPU处理数据的速度就越快。

CPU的主频 = 外频 X 倍频系数

查看型号以及主频:

1
2
$ cat /proc/cpuinfo | grep "model name" | cut -f2 -d: | uniq
Intel(R) Core(TM) i7-3632QM CPU @ 2.20GHz

我的电脑的CPU最高频率可达2.20GHZ。

查看当前执行频率:

1
2
3
4
5
6
7
8
9
$ cat /proc/cpuinfo |grep MHz|uniq
cpu MHz : 1218.250
cpu MHz : 1249.531
cpu MHz : 1208.453
cpu MHz : 1493.250
cpu MHz : 1341.140
cpu MHz : 1427.164
cpu MHz : 1318.023
cpu MHz : 1200.031

由于开启了CPU的节能特性,每次执行的频率都会不一样。CPU会自动调整当前的执行频率。

外频

外频是CPU的基准频率,单位是MHz。CPU的外频决定着整块主板的运行速度。

倍频系数

倍频系数是指CPU主频与外频之间的相对比例关系。在相同的外频下,倍频越高CPU的频率也越高。

CPU缓存

CPU缓存(Cache Memory)是位于CPU与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。

高速缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存。

在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速度。

CPU缓存可以分为一级缓存,二级缓存,部分高端CPU还具有三级缓存,每一级缓存中所储存的全部数据都是下一级缓存的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。

在操作系统查看CPU缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ lscpu
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 6144K

$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
$ cat /sys/devices/system/cpu/cpu0/cache/index1/size
32K
$ cat /sys/devices/system/cpu/cpu0/cache/index2/size
256K
$ cat /sys/devices/system/cpu/cpu0/cache/index3/size
6144K

附/proc/cpuinfo

不同指令集(ISA)的CPU产生的/proc/cpuinfo文件不一样,基于X86指令集CPU的/proc/cpuinfo文件包含如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
processor	: 0
vendor_id : GenuineIntel
cpu family : 6
model : 58
model name : Intel(R) Core(TM) i7-3632QM CPU @ 2.20GHz
stepping : 9
microcode : 0x15
cpu MHz : 1288.289
cache size : 6144 KB
physical id : 0
siblings : 8
core id : 0
cpu cores : 4
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb pln pts dtherm tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms xsaveopt
bugs :
bogomips : 4389.72
clflush size : 64
cache_alignment : 64
address sizes : 36 bits physical, 48 bits virtual
power management:

  • processor : 系统中逻辑处理核的编号。对于单核处理器,则可以认为是其CPU编号,对于多核处理器则可以是物理核、或者使用超线程技术虚拟的逻辑核
  • vendor_id : CPU制造商
  • cpu family : CPU产品系列代号
  • model : CPU属于其系列中的哪一代的代号
  • model name : CPU属于的名字及其编号、标称主频
  • stepping : CPU属于制作更新版本
  • microcode : CPU微码
  • cpu MHz : CPU的实际使用主频
  • cache size : CPU二级缓存大小
  • physical id : 单个CPU的标号
  • siblings : 每颗物理cpu的逻辑核数,与cpu cores对比可以确认cpu是否启用超线程
  • core id : 当前物理核在其所处CPU中的编号,这个编号不一定连续
  • cpu cores : 每颗物理cpu的核数,即几核CPU,每个物理cpu具有几个运算内核core
  • apicid : 用来区分不同逻辑核的编号,系统中每个逻辑核的此编号必然不同,此编号不一定连续
  • initial apicid: 0
  • fpu : 是否具有浮点运算单元(Floating Point Unit)
  • fpu_exception : 是否支持浮点计算异常
  • cpuid level : 执行cpuid指令前,eax寄存器中的值,根据不同的值cpuid指令会返回不同的内容
  • wp : 表明当前CPU是否在内核态支持对用户空间的写保护(Write Protection)
  • flags : 当前CPU支持的功能
  • bogomips : 在系统内核启动时粗略测算的CPU速度(Million Instructions Per Second)
  • clflush size : 每次刷新缓存的大小单位
  • cache_alignment : 缓存地址对齐单位
  • address sizes : 可访问地址空间位数
  • power management: 对能源管理的支持

公司前端开发架构改造

现在的前端早已不是几年前的前端,再也不是jQuery加一个插件就能解决问题的时代。

最近对公司前端的开发进行了一系列的改造,初步达到了我想要的效果,但是未来还需要更多的改进。最终要完美的实现目标:工程化模块化组件化

这是一个艰难的,持续的,不断进化的过程!

先说下我司前端改造前的情况:

开始的时候,只有微信公众号开发,以及在APP中嵌入的Web页面,只需要考虑微信端的问题,以及跟原生APP的交互处理,一切都好像很完美。

几个月后,要开发手机网页版,接着百度直达号也来了。原来微信端的功能已经比较多,不可能针对这2个端再重新开发,于是把微信端的代码拷贝2份,然后修改一下就上线了(初创公司功能要快速上线,有时不会考虑太多的技术架构,都懂的)。

这样就出现了6个不同的项目文件夹,为什么会是6个呢?因为也分别拷贝出了各自的测试目录:

1
2
3
4
5
6
/wap
/waptest
/m
/mtest
/baidu
/baidutest

于是,问题就来了,开发的时候,都在其中一个test目录下开发,比如waptest,开发测试没问题了,就拷贝修改的代码到其它的目录。这样开发的痛苦,可想而知了。

不仅仅是各个端的js,css,images文件是分别存放,还有各个端的页面模板也是在各自的目录下。

另外,一直以来,公司的前端美女会使用grunt做一些简单的前端构建,比如sass编译,css合并等,但离我想要的前端自动化/工程化还是有点远。

为了提高前端的工作效率,最近终于有一点时间腾出手来处理这些问题。

PS:我们团队组建一年多,项目也从0开始,到现在为止,产品/开发/项目管理等都在逐渐完善。走专业化道路,是我一直以来的目标,有兴趣的可以加我一起交流!

问题总结

先来总结一下改造前前端开发存在的问题:

  1. 同时存在多端,造成开发效率不高
  2. 项目没有模块化,组件化的概念,代码复用率低
  3. 部署困难,没有自动生成版本号,每次都要手动修改js的版本号
  4. 面条式的代码,开发任务重,没有做很好的规划

改进目标

有问题,那就想办法去解决它:

  1. 解决多端统一的问题,一处修改,多端同时生效
  2. 模块化开发,使代码逻辑更加清晰,更好维护
  3. 组件化开发,增强扩展性
  4. 按需打包,以及自动构建
  5. 自动更新js版本号,实现线上自动更新缓存资源
  6. 紧跟发展趋势,使用ES6进行开发

在改进的过程中,会用到2个工具: GulpWebpack。用这2个工具,也是有原因的。

本来我想在Grunt的基础上利用Browserify进行模块化打包,但是发现打包的速度太慢,我的Linux系统编译要4s以上,美女前端的Widnows系统一般都要7s以上,这简直不能忍受。在试用Gulp之后发现速度杠杠的,不用想了,立刻替换Grunt。至于Webpack,是因为用browserify打包多个入口的文件配置比较麻烦,在试用了Webpack之后,发现Webpack的功能比browserify强大很多,于是就有了这2个工具的组合。Webpack的配置比较灵活,但是带来的结果就是比较复杂,在项目中,我也仅仅用到了它的模块化打包。

于是,最终初步实现前端构建的方案是:

Gulp进行JS/CSS压缩,代码合并,代码检查,sass编译,js版本替换等,Webpack只用来进行ES6的模块化打包。

现在前端的操作很简单:

开发的时候,执行以下命令,监听文件,自动编译:

1
$ gulp build:dev

开发测试完成,执行以下命令,进行编译打包,压缩,js版本替换等:

1
$ gulp build:prod

从此,前端开发可以专心地去写代码了!

方案实现

项目结构

整个项目是基于Yii2这个框架,相关的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
common/
pages/
user/
index/
cart/
wap/
modules/
user/
index/
cart/
web/
dev/
index/
user/
cart/
common/
lib/
dist/
logs/
gulp/
tasks/
utils/
config.js
config.rb
node_modules/
index.php
package.json
gulpfile.js
webpack.config.js
.eslintrc

  • common/pages存放公共模板,各个端统一调用
  • web/dev是开发的源码,包含了js代码,css代码,图片文件,sass代码
  • web/dist是编译打包的输出目录

统一多端的问题

由于多端的存在,导致开发一个功能,要开发人员去手动拷贝代码到不同的目录,同时还要针对不同的端再做修改。

js文件,css文件,图片文件,还有相关的控制器文件,模板文件都分散在不同的目录,要拷贝,耗时间不说,而且容易出错遗漏。

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

  • 所有端调用公共的文件
  • 在某个端开发,开发完成之后,用工具自动拷贝文件,并且自动替换相关调用

在综合考虑了之后,这2种方法同时使用,模板文件多端公共调用,其它的文件,通过命令自动拷贝到其它端的目录。

公共模板放到目录common/pages,按模块进行划分,重写了下Yii2的View类,各个端都可以指定是否调用公共模板。

1
2
3
4
5
6
7
public function actionIndex()
{

$this->layout = '/main-new';
return $this->render('index', [
'_common' => true, // 通过该值的设置,调用公共模板
]);
}

模板一处修改,多端生效。

另外,其它文件通过gulp去拷贝到不同的目录,例如:

1
$ gulp copy:dist -f waptest -t wap

这里有一个前提就是,所有编译打包出来的文件都是在dist文件夹,包含了js代码,css代码,图片文件等。

组件化开发

这个只能说是未来努力的一个目标。现阶段还没能很好地实现。这里单独列出这一点,是希望给大家一点启发,或者有哪路高手给我一点建议。

看了网上诸路大神的言论,总结了下前端的组件化开发思想:

  • 页面上的每个独立的可视/可交互区域视为一个组件
  • 每个组件对应一个工程目录,组件所需的各种资源都在这个目录下就近维护
  • 组件与组件之间可以 自由组合
  • 页面只不过是组件的容器,负责组合组件形成功能完整的界面
  • 当不需要某个组件,或者想要替换组件时,可以整个目录删除/替换

其中,各个组件单独的目录,包含了js代码,样式,页面结构。这样就把各个功能单元独立出来了。不仅维护方便,而且通用性高。

最终,整个Web应用自上而下是这样的结构:

模块化开发

前端开发的代码从开始到现在,经历了3个阶段:

  • 第一阶段,面条式代码,在每个模板页面写上一堆js代码,执行的代码跟函数代码交替出现,多重复代码
  • 第二阶段,进化到了Object,每个模板页面的js都用Object进行封装一次
  • 第三阶段,引入ES6的模块化写法

在之前,前端都按下面的目录存放文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js/
goods-order.js
package-order.js
index.js
user.js
zepto.js
crypto-js.js
images/
goods.jpg
logo.png
footer.png
css/
header.css
footer.css
goods-order-index.css

这样会导致一个目录下会有很多文件,找起来非常不方便,而且结构不清晰,比较杂乱。

现在,在目录web/dev分开不同的目录存放各个模块的代码以及相关文件,web/dist是编译打包出来的文件存放的目录。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dev/
lib/
zepto.js
crypto-js.js
common/
js/
request.js
url.js
scss/
head.scss
order/
goods-order/
index.js
index.scss
a.png
package-order/
index.js
index.scss
dist/
lib/
lib.js
order/
goods-order/
index.50a80dxf.js
a.png

其中,有2个重要的目录:

  • lib/目录存放第三方库,编译的时候合并并压缩为一个文件,在页面上直接引入,服务端开启gzip以及缓存
  • common/目录存放公共的模块,包括js的,还有sass等,其它模块目录的js,sass可以调用这些公共的模块

其它的目录都是单独的一个模块目录,根据业务的情况划分,每个模块目录把js,sass,图片文件都放一起。

这样的结构清晰明了,极大地提高了可维护性。

至于JS代码的模块化,刚好去年发布了ES6,而且各大框架/类库/工具等都支持ES6的特性,显然这是未来的一种趋势,相比以前的CMD/AMD/CommonJS规范,选择ES6会更加的符合时代的发展。

ES6支持Class以及继承,以及使用import来引入其他的模块,使用起来很简单。

至于CSS的模块化,之前是使用Compass来写CSS,在本次改造中,还没做太多的处理,只是由原来的grunt编译该为用gulp编译。但是compass已经很久没有更新了,而且不建议使用它。以后会逐渐替换掉。

模块化打包

由于使用了ES6的模块化写法,需要引入Webpack进行编译打包,我是gulp与webpack配合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
var gulp = require('gulp');
var webpack = require('webpack-stream');
var changed = require('gulp-changed');
var handleError = require('../utils/handleError');
var config = require('../config');

gulp.task('webpack', function() {
return gulp.src(config.paths.js.src)
.pipe(changed(config.paths.js.dest))
.pipe(webpack( require('./../../webpack.config.js') ))
.on('error', handleError)
.pipe(gulp.dest(config.paths.js.dest));
});

webpack的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
var webpack = require('webpack');
var path = require('path');
var fs = require('fs');
var assetsPlugin = require('assets-webpack-plugin');
var config = require('./gulp/config');
var webpackOpts = config.webpack;

var assetsPluginInstance = new assetsPlugin({
filename: 'assets.json',
path: path.join(__dirname, '', 'logs'),
prettyPrint: true
});

var node_modules_dir = path.resolve(__dirname, 'node_modules');
var DEV_PATH = config.app.src; // 模块代码路径,开发的时候改这里的文件
var BUILD_PATH = config.app.dest; // webpack打包生成文件的目录

/**
* get entry files for webpack
*/

function getEntry()
{

var entryFiles = {};
readFile(DEV_PATH, entryFiles);
return entryFiles;
}

function readFile(filePath, fileList)
{

var dirs = fs.readdirSync(filePath);
var matchs = [];
dirs.forEach(function (item) {
if(fs.statSync(filePath+'/'+item).isDirectory()){
readFile(filePath+'/'+item, fileList);
}else{
matchs = item.match(/(.+)\.js$/);
if (matchs) {
key = filePath.replace(DEV_PATH+'/', '').replace(item, '');
if(!key.match(/^lib(.*)/) && !key.match(/^common(.*)/)){
fileList[key+'/'+matchs[1]] = path.resolve(filePath, '', item);
}
}
}
});
}

var webpackConfig = {
cache: true,
node: {
fs: "empty"
},
entry: getEntry(),
output: {
path: BUILD_PATH,
filename: '',
// publicPath: '/static/assets/',
},

externals : webpackOpts.externals,

resolve: {
extensions: ["", ".js"],
modulesDirectories: ['node_modules'],
alias: webpackOpts.alias,
},

plugins: [
assetsPluginInstance,
new webpack.ProvidePlugin(webpackOpts.ProvidePlugin),
],

module: {
noParse: webpackOpts.noParse,
loaders: [
{
test: /\.js$/,
loader: 'babel',
exclude: [node_modules_dir],
query: {
presets: ['es2015'],
}
},
]
}
};

if(process.env.BUILD_ENV=='prod'){
webpackConfig.output.filename = '[name].[chunkhash:8].js';
}else{
webpackConfig.output.filename = '[name].js';
webpackConfig.devtool = "cheap-module-eval-source-map";
}

module.exports = webpackConfig;

入口文件

项目的入口文件都放在/web/dev下面,根据业务特点来命名,比如:index.jspay.js

webpack.config.js文件,可以通过getEntry函数来统一处理入口,并得到entry配置对象。如果你是多页面多入口的项目,建议你使用统一的命名规则,比如页面叫index.html,那么你的js和css入口文件也应该叫index.jsindex.css

资源映射记录

由于编译出来的文件是带有版本号的,如select-car.b9cdba5e.js,每次更改JS发布,都必须要替换模板页面的script包含的js文件名。

我用到了assets-webpack-plugin这个插件,webpack在编译的时候,会生成一个assets.json文件,里边记录了所有编译的文件编译前后的关联。如:

1
2
3
4
5
6
7
8
9
10
11
{
"store/select-store": {
"js": "store/select-store.54caf1d3.js"
},
"user/annual2/index": {
"js": "user/annual2/index.2ff2c11d.js"
},
"user/user-car/select-car": {
"js": "user/user-car/select-car.cd0f5f41.js"
}
}

这个插件只是生成映射文件,还需要用这个文件去执行js版本替换。看下面的自动更新缓存。

定义环境变量

在开发的时候,编译打包的文件跟发布编译打包出来的文件肯定不一样,具体可以参考构建优先的原则

在gulp的build:devbuild:prod命令里边,会设置一个环境变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gulp.task('build:dev', function(cb){

// 设置当前应用环境为开发环境
process.env.BUILD_ENV = 'dev';

//... ...
});

gulp.task('build:prod', function(cb){

// 设置当前应用环境为生产环境
process.env.BUILD_ENV = 'prod';

//... ...
});

然后在webpack里边根据不同的环境变量,来进行不同的配置:

1
2
3
4
5
6
if(process.env.BUILD_ENV=='prod'){
webpackConfig.output.filename = '[name].[chunkhash:8].js';
}else{
webpackConfig.output.filename = '[name].js';
webpackConfig.devtool = "cheap-module-eval-source-map";
}

自动更新缓存

一直以来,我们修改js提交发布的时候,都需要手动去修改一下版本号,如:

当前线上版本:

1
<script src="/js/user-car.js?v=1.0.1"></script>

待发布版本:

1
<script src="/js/user-car.js?v=1.1.0"></script>

这样现在看起来好像没有什么问题,唯有的问题就是每次都要手动改版本号。

但是,如果以后要对静态资源进行CDN部署的时候,就会有问题。一般动态页面会部署在我们的服务器,静态资源比如js,css,图片等会使用CDN,那这时候是先发布页面呢,还是先发布静态资源到CDN呢?无论哪个先后,都会有个时间间隔,会导致用户访问的时候拿到的静态资源会跟页面有不一致的情况。

以上这种是覆盖式更新静态资源带来的问题,要解决这个问题,可以使用非覆盖式更新,也就是每次发布的文件都是一个新的文件,新旧文件同时存在。这时可以先发布静态资源,再发布动态页面。可以完美地解决这个问题。

那么,我们要实现的就是每次开发修改js文件,都会打包出一个新的js,并且带上版本号。

webpack中可以通过配置output的filename来生成不同的版本号:

1
webpackConfig.output.filename = '[name].[chunkhash:8].js';

有了带版本号的js,同时也生成了资源映射记录,那就可以执行版本替换了。

在网上看了下别人的解决方案,基本上都说是用到webpack的html-webpack-plugin这个插件来处理,或者用gulp的gulp-revgulp-rev-collector这2个插件处理。但是我感觉都不是很符合我们项目的情况,而且这个应该不难,就自己写了一个版本替换的代码去处理。这些插件后续有时间再研究研究。

在页面模板上,我们通过下面的方式来注册当前页面的使用的js文件到页面底部:

1
2
3
<?php
$this->registerJsFile('/dist/annual2/index/index.js');
?>

每次用gulp执行版本替换的时候, 会先读取资源映射文件assets.json,拿到所有js的映射记录。

1
2
3
4
5
6
7
8
9
10
11
var assetMap = config.app.root + '/logs/assets.json';
var fileContent = fs.readFileSync(assetMap);
var assetsJson = JSON.parse(fileContent);
function getStaticMap(suffix){
var map = {};
for(var item in assetsJson){
map[item] = assetsJson[item][suffix];
}
return map;
}
var mapList = getStaticMap('js');

然后再读取模板文件,用正则分析出上面注册的js文件,最后执行版本替换就行了。

一些要点

使用externals

项目一般会用到第三方库,比如我们会用到zeptojsart-templatecrypto-js等。

单独把这些库打包成一个文件lib.js,在页面上用script标签引入。

这可以通过在webpack中配置externals来处理。

sourcemap

在开发环境下,可以设置webpack的sourcemap,方便调试。 但是webpack的sourcemap模式非常多,哪个比较好,还没什么时间去细看。 可以参考官方文档

最后

至此,前端项目的第一阶段的改造算是完成了。 我不是前端开发,gulpwebpack都是第一次接触然后使用,中间踩了不少的坑,为了解决各种问题,差不多把google都翻烂了。不过庆幸的是,现在前端开发可以比较顺畅地去写代码了。整个结构看起来比以前赏心悦目了不少。

我觉得这次改造最大的变化不是使用了2个工具使到开发更自动化,而是整个开发的思想与模式都从根本上发生了变化。未来还会继续去做更多的探索与改进。

各位看官对前端开发有更好的建议或者做法,欢迎随时跟我交流。

npm使用记录

如今每个语言体系中都有一个包管理工具,PHP的Composer,Ruby的gem,Python的pip,Java的Maven……。当然还有Node.js的npm。

npm本来是Node.js的包管理工具,但随着JS这几年的蓬勃发展,现在的npm已经成了几乎所有跟JS相关的工具和软件包的管理工具了,并且还在不断发展完善中。

要安装npm,直接安装完Nodejs就自带有npm了。 但是npm更新比较快,所以安装完Nodejs后,直接更新npm到最新版:

1
$ sudo npm install npm@latest -g

在本文撰写之时(2016年4月11日)官方最新版是3.8.6

1
2
$ npm -v
3.8.6

关于NPM

npm可以说是Node获得成功的重要原因之一。所以每个写JS的人都应该要懂npm。

package.json

package.json文件描述了一个NPM包的所有相关信息,包括作者、简介、包依赖、构建等信息。格式必须是严格的JSON格式。

通常我们在创建一个NPM程序时,可以使用npm init命令,通过交互式的命令,自动生成一个package.json文件,里面包含了常用的一些字段信息,但远不止这么简单。通过完善package.json文件,我们可以让npm命令更好地为我们服务。

一般我们都在项目下生成一个package.json文件,用来管理npm的包依赖。

使用以下命令来生成package.json文件:

1
2
$ npm init
$ npm init -y

该文件里边的内容就不细说了,可以自行去搜索。这里只记录下包版本的规则说明:

  • version 完全匹配
  • >version 大于这个版本
  • >=version 大于或等于这个版本
  • <version
  • <=version
  • ~version 非常接近这个版本
  • ^version 与当前版本兼容
  • 1.2.x X代表任意数字,因此1.2.1, 1.2.3等都可以
  • http://... Unix系统下使用的tarball的URL。
  • * 任何版本都可以
  • "" 任何版本都可以
  • version1 - version2 等价于 >=version1 <=version2.
  • range1 || range2 满足任意一个即可
  • git... Git地址
  • user/repo

安装模块

npm install命令用来安装模块到node_modules目录。

1
2
3
4
5
$ npm install jquery
$ npm install jquery -f # 强制重新安装
$ npm install jquery --save
$ npm install jquery --save-dev
$ npm install jquery -g

更新已安装模块,就要用到npm update命令。

1
$ npm update jquery

查看模块的信息:

1
$ npm view crypto-js

缓存目录

npm installnpm update命令,从registry下载压缩包之后,都存放在本地的缓存目录。

这个缓存目录,在 Linux 或 Mac 默认是用户主目录下的.npm目录,在 Windows 默认是%AppData%/npm-cache。通过配置命令,可以查看这个目录的具体位置。

1
2
$ npm config get cache
/home/lixiang/.npm

在缓存目录你会看到里面存放着大量的模块,储存结构是{cache}/{name}/{version}

清空缓存目录,可以:

1
2
3
$ rm -rf ~/.npm/*
# 或者
$ npm cache clean

安装过程

总结一下,Node模块的安装过程是这样的。

  1. 发出npm install命令
  2. npm 向 registry 查询模块压缩包的网址
  3. 下载压缩包,存放在~/.npm目录
  4. 解压压缩包到当前项目的node_modules目录

注意,一个模块安装以后,本地其实保存了两份。一份是~/.npm目录下的压缩包,另一份是node_modules目录下解压后的代码。

但是,运行npm install的时候,只会检查node_modules目录,而不会检查~/.npm目录。也就是说,如果一个模块在~/.npm下有压缩包,但是没有安装在node_modules目录中,npm 依然会从远程仓库下载一次新的压缩包。

为了解决这些问题,npm 提供了一个--cache-min参数,用于从缓存目录安装模块。

--cache-min参数指定一个时间(单位为分钟),只有超过这个时间的模块,才会从 registry 下载。

1
2
$ npm install --cache-min 9999999 <package-name>
$ npm install --cache-min Infinity <package-name>

模块关系

通过以下的图可以清晰地看到npm安装的模块的依赖关系。

A/B/C/D/E各个模块根据依赖关系,放到各自的目录下。

具体的依赖关系参考官网: https://docs.npmjs.com/how-npm-works/npm3-nondet

命令总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ npm help install
$ npm config list
$ npm config ls -l
$ npm config get prefix
$ npm config set prefix=$HOME/.node_modules_global
$ npm uninstall underscore
$ npm install underscore@1.8.2
$ npm update underscore
$ npm search mkdir
$ npm view mkdir
$ npm install jquery -g
$ npm install jquery --save
$ npm install jquery --save-dev
$ npm init
$ npm install
$ npm dedupe # 重新计算依赖关系,然后将包结构整理得更合理。

开启前端工程化

Web前端技术在最近这2年简直是爆发式的增长。记得在2013年,我抄阿里云OSS的前端代码的时候,还是用jQuery库,用的是Object的写法。

那时候最热门的就是jQueryExtJS等,Node.js还没现在的这么火热(其实我还不知道有Nodejs,我不是做前端开发的,孤陋寡闻了~)。

在去年的时候,开始负责公司的开发,主要集中在移动端的开发。于是,一不小心去研究了下前端,才发现现在的前端早已不是我以前认识的前端。

首先是HTML5正式定稿,Web Page现在已经演变成Web APP了。

然后是ES6也华丽丽地走进了前端,它的 Module/Class 等特性已经完全让这门语言具备了开发大型应用的能力。

还有各种的类库,框架等。尤其是ReactAngularVuejs等框架的出现与争锋,以及Node.js前后端分离的流行,中间层的出现改变了前后端的合作模式。

各种构建工具,包管理工具,模块管理器百花齐放,npmgruntgulpbrowserifywebpack等等,没玩过你就真的落后了。

而移动端的发展也如火如荼,用户体验,性能,多屏适配等越来越重要。

前端已经步入了工业化的生产时代。

为了紧跟时代潮流,有必要去开启我们的前端工业化进程(说的好像有点高大上)。

构建优先

以前,我们会网上复制一些代码片段,然后粘贴到页面上,把javascript代码搅合在一起,能运行就可以收工了。

如今,javascript的代码越来越复杂,开发的web应用越来越庞大。可维护性,可扩展性就变得越来越重要。

如何实现这个目标?

  • 首先,要在还没进行任何编码之前做好设计,让应用结构清晰,易于测试。
  • 另外,对应用采取构建优先的原则,通过自动化的手段去建立一套固定的构建过程。

对应用的设计,网上有很多介绍的文章与方案,本文就暂时不进行总结。对于自动化构建的过程,是现在公司要完善的东西,在此会做比较详细的说明。

构建过程

构建过程有2个目标:

  • 自动完成重复性任务,包括安装依赖,编译代码,运行单元测试,以及执行其它操作等。
  • 随时能部署

要创建构建过程,需要了解应用环境,以及构建模式。

应用环境

一般我们的应用会分为3种环境:

  • 开发环境
  • 过渡环境
  • 生产环境

我们大部分时间都在开发环境中,这个环境要可以持续开发,要易于调试,要能轻易阅读堆栈跟踪,能输出详细的日志等。

过渡环境一般部署跟生产机一样的环境,主要用于测试,确保应用发布到生产环境中不会出现问题。

生产环境是用户能直接访问的环境。

构建模式

以上3种环境的特性决定了我们的构建模式是不一样的。可以把构建模式分为2种:

  • 调试模式
  • 发布模式

调试模式

该模式的目标是效率最大化,便于调试和持续开发。由于开发的时候,我们每写一部分代码就要进行调试,那调试模式有以下的特点:

  • 监控变动,我改动代码的时候,不需要手动再去构建一次,让它自动化
  • 可以生成一些中间产物
  • 可以自动刷新浏览器
  • 构建出来的结果不是最优的

发布模式

该模式的目标是发布面向用户的产品,要测试良好,性能高,运行速度快。具有这些特点:

  • 一切为了性能,构建出来的结果必须是最优的
  • 通过所有测试
  • 便于部署

执行构建过程

开发流程

开发环境采用的一般是调试模式,整个开发流程可以参考下图:

发布流程

代码开发测试完成,需要发布到生产环境,那么发布流程可以参考下图:

总结

对于前端开发,我们采取构建优先的原则,针对不同的应用环境,去建立一套完整的构建过程,实现过程自动化,提供持续开发,持续集成以及持续部署的功能,增强架构。

具体的构建方案,在后续的文章中会陆续有提及,敬请关注!

使用Grunt进行前端自动化构建

先来看下我们的一些开发场景:

项目开始前:

新建项目文件夹,创建js,images,css文件夹;拷贝js库(jquery,seajs等)和css库(bootstrap等)进入相关的文件夹,再新建各个文件。

写代码中:

编辑器编码 --> 切换到浏览器F5 --> 编辑器编码 --> 切换到浏览器F5 --> 编辑器编码 --> 切换到浏览器F5 ......

编码完成:

HTML去掉注析、换行符

CSS文件压缩合并

JS代码风格检查

JS代码压缩

image压缩

这么多重复的工作,简直是太浪费时间了。为了解决这些问题, Grunt应运而生。

Grunt是javascript的一个自动化构建工具,你可以用它来做一些重复的任务,比如压缩、编译、单元测试、linting等,配置好Grunt的配置文件Gruntfile.js,你就可以专心地去写代码了。

一些链接:

  • 官网: http://gruntjs.com/
  • Github: https://github.com/gruntjs

本指南使用的grunt版本是v0.4.5

安装

Grunt的组成

Grunt有三大主要组成部分:

  • Gruntjs CLI: GruntJS的命令行工具,用于调用与Gruntfile在同一目录中的Grunt。
  • Grunt: 用于执行任务。
  • Grunt Plugins: Grunt的很多任务都是通过安装不同的插件去实现。

Grunt CLI与Grunt是分离的, 安装Grunt CLI不等于安装了Grunt。一般都是全局安装Grunt CLI,而在项目目录中安装Grunt。

这样就能让多个版本的 Grunt 同时安装在同一台机器上,同时确保每个团队的成员在同一项目使用同一版本的Grunt,避免未来不同项目对Grunt不同版本的依赖关系。

安装Grunt

1)升级npm

可以通过npm安装Grunt,nodejs的版本必须要>=0.8.0。在开始安装Grunt之前,先升级npm:

1
$ npm update -g npm

或者:

1
$ npm install -g npm@latest

2)安装grunt-cli

全局安装Grunt CLI:

1
2
$ npm install -g grunt-cli
$ grunt --version

**注:**安装了Grunt CLI并不等于安装了Grunt

3)安装grunt

Grunt作为项目的开发依赖进行安装。在你的项目目录安装:

1
2
$ npm install grunt --save-dev
$ npm install grunt@0.4.5 --save-dev # 指定安装的版本

开始使用

现在来开始使用Grunt。我们以使用jshint为例。

首先安装好jhsint以及Grunt的jshint插件:

1
2
$ npm install -g jshint
$ npm install --save-dev grunt-contrib-jshint

然后在项目根目录下新建Grunt的配置文件Gruntfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14

module.exports = function(grunt) {

// 配置Grunt各种模块的参数
grunt.initConfig({
jshint: [‘Gruntfile.js’]
});

// 从node_modules目录加载模块文件
grunt.loadNpmTasks('grunt-contrib-jshint');

// 注册check别名,执行jshint任务
grunt.registerTask('check', ['jshint']);
};

在命令行执行:

1
$ grunt check

这样我们就可以通过grunt去使用jshint来检查javascript代码了。

应用

为了更好地管理grunt的任务,我不把所有的task配置都写在Gruntfile.js,而是把各个任务的配置分离到不同的配置文件。这需要使用到插件load-grunt-tasks,它会自动读取并加载项目packge.json文件中devDependencies配置下以grunt-*开头的依赖库。。

1
$ npm install load-grunt-tasks --save-dev

然后,可以这样写Gruntfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var glob = require('glob'),
path = require('path'),
join = path.join;

module.exports = function(grunt) {

var config = {
pkg: grunt.file.readJSON('package.json'),
};
grunt.util._.extend(config, loadConfig('./grunt/options/'));
grunt.initConfig(config);

require('load-grunt-tasks')(grunt);

grunt.loadTasks('grunt');
};

function loadConfig(configPath) {
var config = {};

glob.sync('*', { cwd: configPath })
.forEach(function(configFile) {
var prop = configFile.replace(/\.js$/, '');
config[prop] = require(join(__dirname, configPath, configFile));
});

return config;
}

同时,新建grunt/文件夹,结构如下:

具体的每个task放options/文件夹下面。例如:

browserify.js

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = exports = {
dist: {
options: {
"transform": [["babelify", { "presets": ["es2015"] }]]
},
files:[{
expand: true,
cwd: 'dev/',
src: ['**/*.js', '!lib/**/*.js'],
dest: 'dist/',
}]
}
};

这样使用是不是觉得很清爽,并且非常容易维护?

参考: More maintainable Gruntfiles

配置文件说明

Grunt配置

Gruntfile.js分几个部分:

"wrapper" 函数

每一份 Gruntfile (和grunt插件)都遵循同样的格式,你所书写的Grunt代码必须放在此函数内:

1
2
3
module.exports = function(grunt) {
// Do grunt-related things in here
};

项目与任务配置

大部分的Grunt任务都依赖某些配置数据,这些数据被定义在一个object内,并传递给grunt.initConfig方法。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
build: {
src: 'src/<%= pkg.name %>.js',
dest: 'build/<%= pkg.name %>.min.js'
}
}
});

加载grunt插件和任务

很多常用的任务(task)都已经以grunt插件的形式被开发出来了。

使用的时候,用npm install安装之后,都可以在Gruntfile.js中以下面的简单命令形式加载使用:

1
2
3
4
5
6
// 加载能够提供"uglify"任务的插件。
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-contrib-compass');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-watch');

自定义任务

以下方式可以定义任务:

1
2
3
grunt.registerTask('default', ['jshint','uglify']);
grunt.registerTask('dev', ['watch']);
grunt.registerTask('prod', ['create']);

然后在命令行就可以执行任务:

1
2
3
$ grunt
$ grunt dev
$ grunt prod

任务的更详细的配置参考: http://gruntjs.com/creating-tasks

grunt进行ES6的模块化编译的时候,速度太慢了,已经转用了gulp,那速度简直是杠杠的。

,