AMD的一道面试题

模块化现在应该已经成为了稍微复杂一点前端开发的标配了。在es6中,都已经支持了的模块化。

之前的面试中,一直感觉模块化AMD,CMD没有什么可以问的,不过昨天面试突然想到一个题目:
对于一个AMD的模式下

文件d.js如下

1
2
3
4
5
6
7
8
define(function (require) {
// ... 很多代码
require('a');
// ... 很多代码
require(['b'], function (b) {});
// ... 很多代码
require('c');
});

a.js,b.js,c.js 文件分别是什么时候加载的,如何加载的?

题目不难

答案是a.jsc.js 是在加载完d.js后就加载。
b.js是在执行到这一行时异步加载的。

具体分析:

我没有看过require.js的源码,我们使用的是esl.js(也是一个AMD的模块加载器),但是他们的实现原理应该差不多。

我从esl.js的角度解读一下:

同步加载,异步加载

首先大家需要知道AMD里面一个同步加载和异步加载的概念。

从概念上面理解,同步就是当我执行到require('a');时,我需要同步的执行a.js里面的内容,也就是需要在执行到这句话时a.js必须已经加载好了,这样才能到达同步。

而对于 require(['b'], function (b) {});,我执行到这一步时,是异步的发出请求,然后异步等待b.js的返回+执行。

同步加载的实现原理

我们从概念上面理解的同步加载的原理,现在看看esl.js的实践。
这里面需要处理两个核心步骤

  1. 执行到require('a');时,a.js必须已经加载好了;
  2. a.js文件里面的所有require('*'),也都必须加载好了,保证在执行a.js时,所有a.js依赖的同步文件都能同步执行;

对于第一步的实现,大概原理是这样的,在加载好了d.js后,会正则匹配一次文件里面的同步依赖require('*');,例如匹配出了 a.jsc.js,然后继续加载a.jsc.js

对于第二部,其实就是一个递归处理,直到没有下一步的依赖为止。

同步加载另外一种处理方法

上面有一部正则逻辑,可见如果使用这种方式,在执行代码前,js需要全部正则一次所有模块化代码的。这样性能是不是有一个无谓的耗损。

那么我们一般怎么处理了?

大家一般都了解过打包编译,例如在使用Requirejs时,线上环境的代码会经过r.js处理一次。

那么d.js文件应该会处理如下

1
2
3
4
5
6
7
8
9
10
11
define(
'path/b',
['require', 'a', 'b'],
function (require) {
// ... 很多代码
require('a');
// ... 很多代码
require(['b'], function (b) {});
// ... 很多代码
require('c');
});

define方法会增加第一个和第二个参数

第一个参数是按照路径生成一个具名id
第二个参数是此文件所依赖的同步文件

这时当模块在解析这个b,js文件时,发现如果存在第二个参数,就会直接解析所需依赖部分,而省去了正则这一步。

我们正则这一步转换到了打包编译中去分析,这样就省掉了浏览器加载时去正则所有AMD文件这一步。

那么为什么我们不在开发环境中直接使用['require', 'a', 'b']方式,我理解目的是为了提高开发便捷性,我们不需要再增加一个require('*')都在中括号内配置一次,同样删除时也不用去删掉配置。

因为这一步完全可以在编译时处理。

打包编译的延生

不知道大家有没有看过编译后的代码和开发环境代码的区别,对于这个b.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
define(
'path/a',
['require'],
function (require) {
// ... 很多代码
});

define(
'path/c',
['require'],
function (require) {
// ... 很多代码
});

define(
'path/b',
['require', 'a', 'b'],
function (require) {
// ... 很多代码
require('a');
// ... 很多代码
require(['b'], function (b) {});
// ... 很多代码
require('c');
});

上面可见,a.jsc.js这两个文件被合并到了d.js中,所有文件都加上了具名id。而且这个id的生成规则是更具路径生成的。

而我们异步加载的b.js文件就没有被打包进来。这是因为我们期望b.js是懒加载的,当使用时在加载,这样也能达到按需加载的目的。

微信公众号

前端修炼

前端发展论战

最近很热的讨论

关于『真阿当』对目前流行前端技术的批判 https://www.zhihu.com/question/38924821

Winter - 我眼中的前端框架jQuery,Angular,React,Vue——以及我看前端架构http://weibo.com/p/1001603924826640228007

关于前端工具变化过快的讨论 https://www.zhihu.com/question/34449620

我感觉到的前端变化 http://bbear.me/wo-suo-gan-jue-dao-de-qian-duan-bian-hua/

上面几篇文章对于前端的发展讨论较多。

前端变化过快的看法

首先,不得不承认前端变化确实太快,对于我而言,react还在了解,没有真正的落地业务时,vue又开始兴起,马上angular2可能又会开始火。。。

变化如此之快,我们该如何面对?

第一,我觉得首先需要避免盲目追新,如果对于新框架只是简单的写写demo,意义是不大的。

为什么了?如果没有复杂的项目支撑,你会踩不到框架的坑,你不会体会到框架哪里设计的巧妙,哪里设计的不足。你也很少有机会为了研究巧妙的实现去看部分的源码。而这个过程其实是使用框架的精髓。

第二,更加深入的加强基础技能,框架会不断更新,更新也会越来越快,只有不断强化一些基础技能,才能够很快的去了解新框架,达到新框架即学即用的能力。

如何提高基础技能呢?就研究下你现在用的框架,或者找一个你觉得很好的框架,深入研究下他的设计思路,源码等等,反复研究,反复体会,花上3个月深入研究一个。当你研究透了,你在看其他框架,相信我,你看的角度会变。

我们对于新技术如何使用

现在团队使用的还是百度自己一套mvc框架,但是当我们在尝试新技术时,我们其实可以很快的即学即用的,es6,react,vue这些的使用并没有什么障碍。

即使我们目前工作都是集中在pc端,我们团队的成员也是可以迅速上手移动端的开发的,我理解很大一部分因素都是基础能力比较扎实。

所以我感觉,请放心,如果没有用到最新的技术,不要害怕。

前端的发展

个人感觉未来的前端更加偏向解决方案的方式,一个合格的架构师能够根据业务,以及开发成员的状态,选择最合适的开发方式,合作方式。

未来的框架,工程化方案会越来越多,你需要做到的是,能够即插即用的能力。在面对一个新框架时,能够快速判断出框架是否适合于业务,是否能提高开发效率。

回到阿当的微博

sass和less最近是不是被提起得少了?backbone呢?响应式设计呢?今天说得起劲的angular和rect,是不是半年后也逐渐消停了呢?一切不接地气的性价比不高的伪高端,都会消停的。我相信jquery还能坚挺5年,不相信rect和angular能热过两年。踩jquery的一直不会停,新时髦也不会停。话放在这儿,两年后咱看看。

我理解,大家不要把注意力放到各种各样的框架上,打好基础,什么新框架都能hold住,岂不是最好。

微信公众号

前端修炼

使用node子进程spawn,exec踩过的坑

如何在项目中实现热更新中提到的一个坑child_process的exec使用问题,下面文章会详细介绍下,debug到node源码中的详细介绍,不容错过。

child_process介绍

Nodejs是单线程单进程的,但是有了child_process模块,可以在程序中直接创建子进程,并使用主进程和子进程之间实现通信。

对于child_process的使用,大家可以找找其他文章,介绍还是比较多的,本文主要讲一下踩过的坑。

踩过的坑

在使用EHU(esl-hot-update)这个工具时(对于工具的介绍,参考前面的文章如何在项目中实现热更新),发现用子进程启动项目,经常性的挂掉。然后也不知道为什么,甚至怀疑子进程的效率比较低。

最后为了进一步验证,在同样的环境下,一个直接启动服务,一个是使用require('child_process').exec('...') 方式启动。

最后发现使用子进程打开还真的就是使用到一定程度就挂掉。虽然此时也没有什么解决方案,但是至少能把问题定位在子进程上了,而不是其他工具代码导致程序挂掉。

定位问题

定位了问题后,网上查找child_process相关资料,发现exec与spawn方法的区别与陷阱 这篇文章提到几点:

  1. exec与spawn是有区别的
  2. exec是对spawn的一个封装
  3. 最重要的exec比spawn多了一些默认的option

基于以上几点有些头绪了,但是还是没有明确的解决方案。

最后一个办法,直接断点到nodejs的child_process.js模块中尝试看看问题出在哪里。

exec和spawn的源码区分

断点进去看后,豁然开朗,exec是对execFile的封装,execFile又是对spawn的封装。

每一层封装都是加强一些易用性以及功能。

直接看源码:

1
2
3
4
5
6
7
8
exports.exec = 
function(command /*, options, callback*/) {
var opts = normalizeExecArgs.apply(null, arguments);
return exports.execFile(opts.file,
opts.args,
opts.options,
opts.callback);
};

exec对于execFile的封装是进行参数处理

处理的函数:

normalizeExecArgs

关键逻辑

1
2
3
4
5
6
7
8
9
10
11
if (process.platform === 'win32') {
file = process.env.comspec || 'cmd.exe';
args = ['/s', '/c', '"' + command + '"'];
// Make a shallow copy before patching so we don't clobber the user's
// options object.
options = util._extend({}, options);
options.windowsVerbatimArguments = true;
} else {
file = '/bin/sh';
args = ['-c', command];
}

将简单的command命名做一个,win和linux的平台处理。

此时execFile接受到的就是一个区分平台的command参数。

然后重点来了,继续debug,execFile中:

1
2
3
4
5
6
7
8
var options = {
encoding: 'utf8',
timeout: 0,
maxBuffer: 200 * 1024,
killSignal: 'SIGTERM',
cwd: null,
env: null
};

有这么一段,设置了默认的参数。然后后面又是一些参数处理,最后调用spawn方法启动子进程。

上面的简单流程就是启动一个子进程。到这里都没有什么问题。

继续看,重点又来了:

用过子进程应该知道这个child.stderr

下面的代码就解答了为什么子进程会挂掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
child.stderr.addListener('data', function(chunk) {
stderrLen += chunk.length;

if (stderrLen > options.maxBuffer) {
ex = new Error('stderr maxBuffer exceeded.');
kill();
} else {
if (!encoding)
_stderr.push(chunk);
else
_stderr += chunk;
}
});

逻辑就是,记录子进程的log大小,一旦超过maxBufferkill掉子进程。

原来真相在这里。我们在使用exec时,不知道设置maxBuffer,默认的maxBuffer是200K,当我们子进程日志达到200K时,自动kill()掉了。

exec和spawn的使用区分

不过exec确实比spawn在使用上面要好很多

例如我们执行一个命令

使用exec

require('child_process').exec('edp webserver start');

使用spawn

linux下这么搞

1
2
3
4
5
6
7
8
9
var child = require('child_process').spawn(
'/bin/sh',
['-c','edp webserver start'],
{
cwd: null,
env: null,
windowsVerbatimArguments: false
}
);

win下

1
2
3
4
5
6
7
8
9
var child = require('child_process').spawn(
'cmd.exe',
['/s', '/c', 'edp webserver start'],
{
cwd: null,
env: null,
windowsVerbatimArguments: true
}
);

可见spawn还是比较麻烦的。

解决方案

知道上面原因了,解决方案就有几个了:

  1. 子进程的系统,不再输出日志
  2. maxBuffer这个传一个足够大的参数
  3. 直接使用spawn,放弃使用exec

我觉得最优的方案是直接使用spawn,解除maxBuffer的限制。但是实际处理中,发现直接考出normalizeExecArgs这个方法去处理平台问题,在win下还是有些不好用,mac下没有问题。所以暂时将maxBuffer设置了一个极大值,保证大家的正常使用。然后后续在优化成spawn方法。

吐槽

其实没有怎么理解,execFile对于spawn封装加maxBuffer的这个逻辑,而且感觉就算加了,是否也可以给一个方式,去掉maxBuffer的限制。

难道是子进程的log量会影响性能?

感想

其实在解决这个问题时,发现这个差异/坑还比较意外,因为自身对于node其实还不是很熟,这个子进程的使用其实也是在ehu中第一次遇到。

感受比较多的就是有时候正对问题去学习/研究,其实效率特别高。

微信公众号

前端修炼

如何在项目中修改代码后实现热更新

本篇文章是最后一篇,主要讲一下在浏览器端的一些实现。和浏览器热更新的细节。

工具源码EHU(esl-hot-update)

浏览器端依赖

socket.io——浏览器端仅仅依赖socket这个去和服务端通信

通信逻辑

1
2
3
4
5
6
7
8
9
// 建立连接
socket.on('hello', function () {
log(getLogMsgPrefix(), 'HotUpdate已启动!');
});
// 检测到文件改动
socket.on('hotUpdate', function (file) {
// log(getLogMsgPrefix(), '检测到文件改动', file);
// ....处理文件修改后对应热更新逻辑
});

对css/less更新的处理

这个原理比较简单,页面监听到样式的修改,重新加载一次样式即可,简单的覆盖。

但是存在一个潜在问题,因为样式是简单的覆盖,所以,如果修改是删除了样式,是无法生效的。

举例:
修改前:

1
2
3
4
5
6
display: none;
overflow: hidden;
position: relative;
background: #FFFFFF;
border: 1px solid #E8E8E8;
margin-top: 20px;

修改后:

1
2
3
4
display: none;
overflow: hidden;
position: relative;
background: #FFFFFF;

删除的bordermargin-top其实是没有生效的

这个也是后期需要解决的一个问题。

对模板更新的处理

目前项目中使用的是tpl的模板引擎。

现在就遇到一个问题,在热更新时,模板引擎其实是重复加载模板的,那么就涉及到重复加载是否后面的会覆盖前面问题。

查看加载模板的源码后,发现根据配置有三个选择,覆盖忽略报错, 我们业务中使用的配置是遇到重复后会报错处理,所以我们需要在不修改业务默认属性的情况下,添加一些逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// [esl-hot-update] 重新加载需要覆盖
window.EHU_HOT_UPDATE_OPTIONS
&& window.EHU_HOT_UPDATE_OPTIONS.etpl.isOverride
&& (namingConflict = 'override');
switch (namingConflict) {
/* jshint ignore:start */
case 'override':
engine.targets[name] = target;
context.targets.push(name);
case 'ignore':
break;
/* jshint ignore:end */
default:
throw new Error('Target exists: ' + name);
}

window.EHU_HOT_UPDATE_OPTIONS.etpl.isOverride这个是修改后自己实现的控制配置修改的逻辑。

然后这个文件加入到服务端的路由中,请求时替换。

对js更新的处理

这里逻辑比较复杂,因为需要修改底层的AMD模块加载的逻辑。

js没有模板那么简单,不是直接覆盖,因为在AMD模式中,每一个文件,都是被上一个文件调用执行的结果。

所以我们处理的逻辑是不仅需要重新加载修改的文件,并且递归所有直接或者间接调用他的文件,全部重新加载。

所以从上面的特点可以看出,这个工具目前阶段主要适用于业务模块的开发,因为业务的依赖不会特别深,对于dep中的核心文件修改,就不是很合适,一旦文件比较底层,热跟新是重新加载的模块也会非常多。

另外也有很多其他的坑,还在不断优化中。

总结

这次实践其实就是业务中遇到的问题(系统太庞大,调试太麻烦),如何解决问题,如何把解决的思路变成一个解决方案,分享给团队。

因为自己解决了,和形成一个解决方案还是有非常大的差别的,例如我们在形成方案的过程中,就尝试了很多新东西,踩了很多坑。

目前还有个坑就是chrome浏览器,调试的Source资源时,如果一个资源重复加载,内存中会更新,但是对应的资源没有更新,导致断点时,映射不对(断点失效),目前暂时的解决方案是,每次请求时添加时间戳,让Source映射的资源强制更新。这个可以正常断点,但是断点没有记忆功能(坑啊,因为文件变了)。

微信公众号

前端修炼

如何在项目中修改代码后实现热更新

上一篇文章说的是一个大体的概括,本篇主要分享一些node层面实现的细节。

工具源码EHU(esl-hot-update)

如何使用

npm install -g ehu(mac下需要sudo,windows下需要管理员权限)

在原来执行edp webserver start命令的路径 执行 ehu(不再需要执行 edp webserver start)

原来端口号8848修改为8844(原8848依旧可以使用,但不支持热更新)

首先使用的方式很简单,为此特意将工具打包到npm上,以后就算有升级,仅仅需要大家update即可。

另外从使用角度,也尽量集成化(一句命令行即可),避免为了这个工具的使用而做太多额外的事情。

依赖的框架

1
2
3
4
5
6
7
8
9
"dependencies": {
"async": "^1.5.0",
"commander": "^2.9.0",
"express": "^4.13.3",
"express-http-proxy": "^0.6.0",
"lodash": "^3.10.1",
"socket.io": "^1.3.7",
"watch": "^0.16.0"
}

几个必要的
watch——监听文件变化
socket.io——和浏览器的实时通讯
express——搭建一个服务
express-http-proxy——代理
commander——便于自己写node命令

工具类:
asynclodash

框架的思想

先看看昨天对于这个工具提出的几个要求

  1. esl必然是需要修改的,但是如何对开发人员透明?首先是不能让大家都做这种修改。
  2. 页面中也必须加入socket.io支持,那么我们如何在不影响其他人员开发的情况下加入?
  3. 我们做的属于beta版本,如何选择性的使用?ehu工具和以前的开发模式随意切换?
  4. 安装方便,能否只是作为一个工具,即插即用,不需要繁琐的配置?

对于1和2,我们其实是需要修改/添加一些代码的,但是代码都不希望提交到项目的开发环境,因为这些代码生成环境完全不需要。

所以我们的解决方案是:拦截,改写(偷梁换柱)

举个例子,当我们需要对esl做一些改造时,我们处理方式是当路由指向esl.js时,我们换成另外一个esl-ehu.js(esl-ehu.js是对esl.js改造后的)返回去,这样就对开发环境的代码透明了。

socket.io的支持也是同理,我们可以在返回html时,改写html的代码,加入对于socket.io的引入。

上面的思路其实来源于之前项目构建打包。

对于3,我们希望在使用工具时,任然能很快切换到以前模式,这样做兼容的目的是希望工具更有竞争力,能吸引大家使用。

我们的解决方案是:内部实现一个子线程,端口号依然是以前的,而且访问这个端口,就绕过了这个工具。

对于子线程child_process,我们还遇到一个问题,就是子线程跑系统的时候,经常挂掉,今天刚刚找到一个解决方案,后面会单开一个文章讲这个坑。

对于4,其实就是使用npm方式

技术细节

第一步:搭建一个新服务作为底层,去托管住我们现在edp服务,新服务上有一个路由配置,对于我们需要处理的,拦截。对于不用处理的直接代理给edp

代码参考

1
2
3
4
5
6
7
8
9
10
11
12
var mid = express();
mid.all('*', httpProxy(config.defaultServer, {
// 先走特殊规则,否则就代理到默认web server
filter: function(req, res) {
return !ruleRoute(req, res);
},
forwardPath: function(req, res) {
return URL.parse(req.url).path;
}
}));
// 由express-http-proxy托管路由
app.use('/', mid);

ruleRoute就是一些拦截处理

在此之前,启动下子进程

1
2
3
4
5
6
var child = require('child_process');
var cli = child.exec(defaultServerCLI);
cli.stdout.on('data', function (log) {
!isServerStarted && (cb(null, log));
isServerStarted && console.log(log);
});

此处有坑,后面单开文章描述

第二步: 因为上面拦截后的返回的文件已经支持socket.io,esl等底层已经修改了,所以下面是需要去监听文件通知浏览器做对应处理。

1
2
3
4
5
6
7
// 启动socket.io服务
io = require('socket.io')(server);
io.on('connection', function (socket) {
socket.emit('hello');
});
// 监视文件改动
initWatch();

第三步: 做一些集成工作

1
2
3
4
5
6
program
.version('0.0.6')
.usage('[options]')
.option('-p, --port <n>', 'Set the port', setPort)
.option('-n, --noServerCLI', '...', noServerCLI)
.parse(process.argv);

集成到node命令中

第四步: 默认配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
// 默认的服务器
defaultServer: 'http://127.0.0.1:8848',
// 默认的服务器启动命令
defaultServerCLI: 'edp webserver start',
// 从服务器根目录到需要监控的文件夹中间path
baseDir: 'nirvana-workspace',
// hot update 需要watch的文件夹(不包括baseDir)
watchDirs: 'src',
// 入口文件(不包括baseDir)
indexHTML: 'main.html',
// ehu启动端口号(不可与默认的服务器端口号冲突)
port: 8844
};

源码中有很多逻辑是处理配置的

最后一篇预告

分享一下esl等基础框架的改造,和浏览器监听到通信后的修改。

微信公众号

前端修炼

如何在项目中修改代码后实现热更新

这个是组内一位同学在平时开发中,发现调试不便,为团队开发的热更新工具。很厉害,文章中的技术实现内容也是我了解了他的具体实现思路后,整理出来的。

工具源码EHU(esl-hot-update)

热更新是什么

热更新就是当你在开发环境修改代码后,不用刷新整个页面即可看到修改后的效果。

如果你的项目中使用了webpack的话,你会很幸运,借助webpack-dev-server插件可以实现项目的热更新。

解决的问题

对于大型的系统级别项目会有下面几个特点

  1. 模块化(AMD)模式的广泛使用后,开发环境散文件特别多,很容易上百,一不小心还能上千
  2. 初始化的内容特别多,各种底层库,ui库等等

这两个特点直接导致每次调试后,刷新会很慢。如果初始化的js达到上千的数量级,每一次重新刷新都是5s,10s,甚至20s的等待。

而热更新的目的就是为了在一定程度上减少这5s,10s,甚20s的浪费。

遇到的问题

  1. 我们使用的是百度自己的开发环境工具edp,首先他不支持热更新
  2. 我们使用的AMD实践也是百度自己的esl,而且即使是requirejs也暂时没有找到对应的热更新策略,假如requirejs有对应的,我们也无法直接使用

所以最终的结论是我们自己去实现一个基于我们自己业务的。这样我们考虑的面不用太广,并且解决方案的更有针对性,即面向我们现有的业务框架。最重要的是可以尝试修改底层框架做配合。

等待路踩通了,我们再去考虑普适性。

解决的思路

从ehu/package.json 这个文件中,我们就可以看出一些具体的思路

  1. 需要一个watch功能,即能够监听到文件的修改
  2. socket.io通知浏览器处理文件的改变
  3. 修改esl这个文件,达到能够实时更新的效果

当时最简单的考虑,就是文件改变了后,能够通知浏览器,浏览器去重新load这个文件并且执行一次。这个时候再重新去打开这个模块或者功能后,会发现新load的代码在执行后会覆盖上一次的。

所以当时的我的第一直觉是,esl重复require时,如果后面一次会覆盖前面的,那么可以通过简单的覆盖思路去尝试,结果发现覆盖不了。经过验证,发现是esl内部维护了一个map,即require过的模块会存起来。我们如果希望更新这个模块,只能将map中的对应模块名删除。(后面会详细讲述esl的改造)

对于工具的要求

对应这个工具,我当时也提出了几个要求

  1. esl必然是需要修改的,但是如何对开发人员透明?首先是不能让大家都做这种修改。
  2. 页面中也必须加入socket.io支持,那么我们如何在不影响其他人员开发的情况下加入?
  3. 我们做的属于beta版本,如何选择性的使用?ehu工具和以前的开发模式随意切换?
  4. 安装方便,能否只是作为一个工具,即插即用,不需要繁琐的配置?

微信公众号

前端修炼

ES6引入前需要解决的问题

最近项目中的一个模块正式引入的ES6,由于是引入新技术,也遇到了一些问题,下面分享下整个引入流程

为什么要引入ES6

最近在看一些前端解决方案的文章,ES6越来越多的出现在前端方案中。

ES6由于浏览器不支持,在使用上也是和CoffeeScript和TypeScript一样,都需要compile-to-JS。

理由一:
符合未来趋势,angular2就是使用TypeScript实现;
react native 也是可以直接使用es6的语法;

理由二:
提高开发效率(待考证);

理由三:
减少代码量、提高可读性等

但我觉得不仅仅如此,应该会有更多优势。所以需要亲自验证。

引入前考虑最多的事情

从个人的角度,趋势这个东西说不准,减少代码量、提高可读性等这些其实都可以通过规范来完成。

我个人最看重的是效率这块,是否能够真正的提高团队开发效率。

另外在一片文章中看到说facebook.com 都使用了ES6 + babel complile,我心里也安稳了一些。

考虑的第二点就是是否会给整个系统引入技术债务,由于这个是新技术的引入,和之前框架没有任何重叠,而且引入也是选择性的(提供一种可用的环境)。如果未来有较大的升级,我们可以修改compile-to-JS做适配和转换。

最后一个问题是我们项目使用的不是grunt这种,有直接的解决方案,引入可能会有风险。不过庆幸的是,我们发现我们使用的edp已经支持了。其实我们开始已经想好了如果不支持,自己会扩展一些插件去支持。

技术方案

ES6 + babel

需要解决的问题

  1. 开发环境下的浏览器不支持ES6?
  2. 使用babel转换的代码,调试不方便?
  3. 线上环境的代码打包编译怎么处理?
  4. ES6的新特性哪些适合使用?
  5. ES6的新特性是否通过babel转换后还有兼容问题?
  6. 开发效率是否会有提高?
  7. 编译器高亮支持?

下面挨个解决问题

想到一句话

你可以坐以待毙,也可以立刻动手解决问题,解决一个再解决一个,解决了所有问题,你就活下来可以回家了

来自《火星救援》

开发环境下的浏览器不支持ES6?
这个容易,使用babel。

使用babel转换的代码,调试不方便?
确定了sourceMap的方式解决,但是开始没有认真看babel文档,绕了个圈子,最后发现babel有个属性

1
2
sourceMaps:both
filename: pathname.replace(/\.(\w+)$/, '.raw.$1')

传入这个参数sourceMaps传入表示启用;
filename是编译前对应的文件,这里必须给一个和处理的文件名不一样的

1
babel.transform(code, options)

线上环境的代码打包编译怎么处理?
在构建的流程中,加入一个babel-processor的流程即可,加入的时机需要是在模块压缩合并前,其实就是越早越好。

ES6的新特性哪些适合使用?
我们参考
使用ES6进行开发的思考

arrows ★★★
classes ★★★
enhanced object literals ★★★
template strings ★★★
destructuring ★★
default + rest + spread ★★★
let + const ★★★
iterators + for..of ★★
generators ★
unicode ☆
modules ★★
module loaders ☆
map + set + weakmap + weakset ★★
proxies ☆
symbols ★
subclassable built-ins ☆
promises ★★★
math + number + string + array + object APIs ★★★
binary and octal literals ★
reflect api ☆
tail calls ★★

文章推荐的新特性,仅使用三星的。

另外推荐阅读探秘ES6 系列

ES6的新特性是否通过babel转换后还有兼容问题?
团队中又同学正在验证,我们验证的环境是IE9+,ff,chrome,我们最终会使用三星特性加上兼容性ok的。

开发效率是否会有提高?
后面会通过做一个小的新需求,或者重构一个小模块去验证。

编译器高亮支持?
sublime Text 下面
https://github.com/babel/babel-sublime
或者
https://github.com/voronianski/oceanic-next-color-scheme

其实问题就这么多,比想象中简单许多,未来可能还有坑,但是至少我们开始尝试了。

红利

  1. 语法有问题时,编译报错——语法检查
  2. 面向未来——未来很多源码都是预编译类型
  3. 开阔前端思路
  4. 能读懂以后牛逼框架的源码, angular2 使用typescript

微信公众号

前端修炼

sourceMap初探索

sourceMap的偶遇

接触到sourceMap,其实是在调研ES6时发现的。

在调研ES6引入生产环境的可能性,初步觉得引入项目时需要解决的几个问题:

  1. 上线时的打包构建问题
  2. 开发环境编译问题,目前chrome都不兼容es6
  3. 解决了开发环境编译后,调试问题——开发的代码和浏览器跑的代码不一样
  4. 其他没有考虑到的问题等等

其中sourceMap就是在调研第三个问题时,发现的。
想想如果在debug时浏览器显示的是编译后的代码,这个对于调试其实是致命的打击。

而通过目前的调研,发现sourceMap就是这个问题的解决方案。

sourceMap是什么

请大家直接看阮一峰的文章
JavaScript Source Map 详解

原理我简单的理解就是通过map文件产出一个映射关系,将编译后的代码映射到编译前的,然后debug时,浏览器解析的是编译后的,但是呈现给我们的是映射前的开发代码。

sourceMap曾经被忽视

在网上找的一些介绍Source Map的文章,发现Source Map主要是解决一些代码压缩混淆后不好调试的问题。

举个例子,jquery-min调试时非常不方便,但是加上Source Map,可以直接在浏览器中调试jquery压缩前的。

但是真正在开发环境,可能将jquery-min换成jquery,大家觉得会更加方便,无需那么麻烦处理,所以这也是可能没有流行起来的原因。

sourceMap为什么又会被重视

直接拿我遇到的问题说明,ES6语法浏览器执行不了怎么办?

使用babel,ok,babel可以编译成js给浏览器执行。这个时候,编译后的代码必然会和我们开发时的不一样。

这时如果我们希望还和以前使用js一样去调试,怎么办?

只能利用Source Map这种,产生一种映射关系,去将开发中的代码映射到debug工具中。

除了es6,另外一个被看好的typescript(angulejs2使用typescript实现),它也同样存在这个问题(目前没有调研它的调试方式)。

可以预见,这种加入一种编译方式去写js可能是未来的一种趋势。
小小跑题一下,为什么个人觉得是一种趋势,编译做的越牛逼,开发的代码就会越简单,能让开发人员抓住与业务,兼容,适配,自适应的坑都交接编译去解决,大大的提高了工程效率。

sourceMap使用参考

大家看看下面这个链接(需要翻墙):

http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/

里面还有例子是将java编译成js,然后在chrome中使用sourceMap直接debug java的代码。

我对sourceMap的了解

由于最近比较忙,这篇文章只是初步探索,了解的非常非常浅,没有什么干货,后面有一定成果后还会继续更新sourceMap其他方面。

微信公众号

前端修炼

推荐工具地址
http://cafe.baidu.com/

这个工具百度很多团队都在使用,现在开发了一版对外的,我觉得很赞。

我觉得工具的核心功能就是很好的解决项目延期的问题。

我个人对于这个工具的理解。

任务拆解

针对大型项目,任务拆解是非常重要的一块,如果没有合适的拆解,很多问题都会积压到项目deadline才集中暴露,如果暴露的问题太多或者问题难度太大,都会直接导致整个项目的延期。

如何拆解项目:
我们的经验是分为story和task,两个层级。

针对story的定义
就是项目中每一个具体的产品上可感知的功能点,所以story由pm拆分比较合适

对应story的大小
从项目上看,是可以独立验证的一个功能,例如一个图片上传组件,一个筛选区域的完整筛选功能。
从时间上面看,最好不要超过10天。

针对task的定义
就是每个story中具体的开发任务,这个是由开发人员自己拆分
story的拆分个人经验最好是从时间上面去拆分,每个任务最好都是1,2天的工作量,最多不要超过3天。

拆分任务的另外一个红利就是,对于任务拆分的越细,对于需求的理解就越透,很多问题你会发现在拆分时就能暴露出来。
当你将一个20天,30天的任务能细化到每一天做什么时,你会发现,你项目的整体设计就已经出来了。

拆分还有一个红利,当你将任务拆分的特别细致时,越不容易被质疑。这个和确定预算的道理差不多。

每天任务状态跟踪

下面是工具里面的任务跟踪状态图

前端修炼

对于团队之间的合作,每天的任务跟踪可能不用特别细致,跟踪story整体状态即可。
但是对于团队内部,任务进度是务必跟踪到task的,这样能让每一个风险点最快的暴露。
每天都及时确认下当前task的状态,没有问题,大家正常进度开发,出现问题及时解决问题。

问题及时暴露

对于每个项目,特别是时间周期比较长的项目,避免不了会有一些坑,以及一些当初没有预料到的风险。

我们决定不了会遇到什么样的风险,但是我们可以决定一个处理风险的方式。
是风险全部挤压到项目后期集中爆发还是将风险分散于开发的日常任务中?

多人合作

这个工具可以多人合作,不能能够了解到自己的状态,还能同步了解合作方的状态。

疑问

拆分其实就需要花费一定的工作量,那么前期花这么多时间是否值得?
如果平常项目经常遇到delay,或者平常项目经常出现到了项目末期都是出于一种赶工的状态。
我觉得可以考虑引入这种模式。

上周听了厂里高工(T10)的分享,对于下面两点(技术深度和广度、解决问题的方法)有些感触。

技术深度和广度

当我们去扩展我们技术的广度时,我们千万不要成为了一个杂家,什么都学,什么都会,但是确达不到精通。

我们需要的一种能力是,深入的研究一门技术,并且能够抽象出一种学习的方法出来,然后能用这种方法快速适应各种其他技术或者新技术。

当你在某个领域真正深入到一种程度,你会发现再学习其他东西没有那么复杂了,会感觉很多东西非常相似。

所以请沉下心来,将一门技术深入下去。

解决问题的方法/态度

大神提到他之前遇到的大神处理问题的方法,很简单

直接针对问题直接去解决问题

想想我们处理问题的方式:
最简单的处理方式:绕过问题,回避出问题的地方
技巧点的处理方式:通过一些hack的手段间接的去修复问题
牛逼的处理方式: 直接针对问题,找到出问题的根源去解决

平常是可以通过对于解决问题的方法来看出技术实力的。

微信公众号

前端修炼