Category Archives: JS

Highlight template code

Today, an frontend developer want to implement an effect — to highlight template code. As an example, we need to transfer string like  "dfasf${1}${2tes}dfsdf${hello3}blabla"  to an array like:

Here is my solution (in JavaScript):

This function will return an array as we mentioned above.

TypeError: Attempted to assign to readonly property

报错内容:TypeError: Attempted to assign to readonly property.

报错代码:document.getElementById(“mockRecordList”).style = “display:none;”

报错原因:iOS webkit内核的一个bug(https://bugs.webkit.org/show_bug.cgi?id=49739

修改方案:document.getElementById(“mockRecordList”).style.cssText = “……”

该bug影响范围为:iOS 10及以下版本操作系统,iOS 11及以上版本正常。

webpack3项目缩包实践总结

Hybrid app由于开发、更新非常方便,所以应用到的地方非常多。一般大家都是通过http(s)://协议访问web页面,所以其实并不会去考虑每次版本迭代时改动到的文件数量。最近在公司做了两个项目的缩包优化,在我们的场景下,大部分web页面是以资源包的形式下载到用户本地后通过file://协议访问的,如果每次迭代都有很多文件发生了变化,用户可能就需要下载大量的数据(通常是以压缩包的形式下载到客户端本地),在网络一般的时候,这种体验是非常不好的,所以我们会比较关注大家不太关心的增量包大小。这也是本文相对于网上大部分缩包相关文章的区别之处(本文以webpack3构建的项目为例,但思路都是相通的,可以借鉴)。

先说增量是个什么概念。假设你的项目第一版一共有 index.html 、 app.js 、 app.css 、 logo.png 4个文件,现在准备发布第二版,包括以下文件:

  • index.html (与第一版内容不一致);
  • app.js (与第一版内容不一致);
  • app.css (与第一版内容一致);
  • logo.png (与第一版内容一致);
  • tel.png (新增文件);

这里,增量的大小就是由 index.html 、 app.js 和 tel.png 贡献的。这里我们要减少增量大小的话,主要从两个方面去考虑:

  1. 减小单个文件的大小(如果每个改动的文件都很小,整体的增量的大小也会小);
  2. 减少改动的文件数量(如果有个文件很大,但是某次上线时这个文件并没有改动,那么它就可以避免它的增量)。

网上大家提得比较多的那些点就不在这里重复提及了,本文主要讲以下几点。

一、固定moduleId和chunkId

如果你看些webpack3的编译产物,你会发现很多js的开头部分的格式非常一致,大概类似下面这样:

在上面这个例子中,74表示的就是chunkId,后面的1020表示的就是moduleId。这两个id很容易变化,有时候可能你只是新加了一个页面,就导致编译产物中大量的文件发生了变化,但是用diff工具仔细对比后发现很多文件的内容里除了这两个id发生了变化外其他内容完全一致。在我们的场景下,是不希望产生这种diff的,所以就需要找到方法来固定moduleId和chunkId。

固定moduleId的方法非常简单,直接使用webpack3提供的HashedModuleIdsPlugin插件即可,官方的介绍信息在这里:https://webpack.docschina.org/plugins/hashed-module-ids-plugin/,下面是使用示例:

固定chunkId没有官方的现成插件可以用,但是也非常简单,下面这么短短不到20行的代码就可以实现一个固定chunkId的插件了(用不容易变的chunk名代替容易变的chunkId,当然前提需要我们保证chunk的名字不能重复,使其仍旧像是id一样可以保证唯一性):

使用示例:

在上面这两个插件的帮助下,我们重新编译一下产物,会发现开头的地方变成了类似下面这种样子:

二、将较大的公共文件拆成多个减少命中大小

我们可能会通过webpack.optimize.CommonsChunkPlugin插件将某些公共组件/文件抽取出来打包成单个文件,以便减小编译产物中的重复代码量。这一步输出的单个文件通常会比较大,其实我们可以继续进一步的优化,将这里涉及到的文件按目录、功能、文件名首字母(比如讲某个目录下的文件名以a-r开头的文件打包到一起,s-z开头的打包到一起)等我们觉得合适的方式分开输出,就可以避免每次有涉及到公共文件的改动就导致一个很大的增量的问题,因为按这种方式拆分后你的改动很可能只会命中其中个别的文件。我在拆文件时通常会尽量保持每个输出的最终文件大小在100~200k之间(我们的项目除了内置于APP中使用,同时也会被用于微信端,拆太小的话会导致wap站用户通过http(s)://协议访问页面时由于请求数过多影响页面加载速度)。

redux中出现同类名action导致的bug

最近同事发现了一个历史遗留bug,bug的原因是redux store派发了同名action导致的。因为现在习惯了模块化开发,通常下意思会认为只要是不同模块里导出来的变量被用到不同的页面里,那彼此就没啥关系了。所以刚看到这个bug还是愣了一下的,虽然是个简单的bug。应了那句话(我说的),难的东西只有两种,一种是真的难,一种是实在简单到过于出乎意料(以至于往往你会想不到)。

都知道redux会管理一个状态机(store),我们可以通过store.dispatch方法来派发不同的action,redux会根据具体派发的action来决定使用何种reducer来更新state(更新后的state我们可以通过store.getState方法获得)。我们碰到的场景简单化后可以用下面这种方式来描述:

一、代码本身逻辑

在模块A中,我们定义了一个actionA( { type: 'A', payload: productName } )(这里的productName是取当前页产品名然后通过调用生成actionA的函数(action generator)来传入的),在A1页面派发该action后会将state.moduleA.productName的值修改为 productName 并通过localStorage进行本地存储,然后在A2页面,我们通过store.getState().moduleA.productName来获取产品名称并进行后续的购买操作。

在模块B中,我们定义了一个名为actionB( { type: 'B', payload: productName } )(这里的productName是取当前页产品名然后通过调用生成actionA的函数(action generator)来传入的),在A2页面派发该action后会将state.moduleB.productName的值修改为 productName 并通过localStorage进行本地存储,然后在B2页面,我们通过store.getState().moduleB.productName来获取产品名称并进行后续的购买操作。

说明:此处为简化模型,非实际场景,实际场景下如果只是带少量参数且拼接后的完整url长度不至于长到超过get请求url长度限制的情况下,跳到下一个页面时推荐直接通过url传递参数的方式。

二、bug解析

现在我们先访问A1页面派发actionA,然后我们不访问B1页面直接去访问B2页面(比如直接通过修改浏览器地址来访问)。正常来说在B2页面我们要么获取不到产品名称,要么可以获取到的是更早些时候我们访问B1页面时存入本地的产品名称。但是如果actionA和actionB中的 type 字段的值相同时(比如都为 'A' ),我们看看会发生什么。

假设现在我们访问A1页面派发actionA时当前页产品名为’productA1’,因为redux其实不知道什么actionA、actionB的,它只当接收了一个 { type: 'A', payload: productName }(这里productName变量的值为 'productA1' ),然后他就将state.moduleA.productName的值更新为 'productA1' 。但是,因为我们在模块B的代码中也告诉了redux——如果遇到 { type: 'A', payload: productName }这样的action时,更新state.moduleB.productName为 productName。这就导致方位A1页面派发一个 { type: 'A', payload: 'productA1' }后,state.moduleA.productName和state.moduleB.productName的值都被更新成 'productA1' 了。进而当我们直接访问B2页面时,当前页面查询到的产品名为 'productA1' 了,但其实在B2页可能适用的产品名应该是 'productB1' 、 'productB2' 、 'productB3' 等。

三、解决方案

解决方案就一条:避免同名action。

这里所谓的同名action,即用于判断采用何种reducer的“依据”相同的action。在上面例子中,就是指 type 值相同的action,但这只是平时大家比较常用的action key,如果你的action长这种样子: { name: 'actionName', data: data } ,那么同名action就是指 name 值相同的那些action。

避免同名action的措施:在action名中添加模块名前缀起到命名作用域的作用。如上面例子中,我们可以将模块A中的action重命名为:

将B模块中的action重命名为:

 

这样,尽管上面两个action的type值在去掉模块名前缀后均为 'A' ,但是对redux来说已经是完全不同的两个action了,不会出现本文例子中那样的混乱场景了。如果你的模块结果较为复杂,比如模块A1内嵌模块A2,模块A2内嵌模块A3,模块B1内嵌模块B2,模块B2内嵌模块B3。那么只需要在模块名处叠加这些模块名作为模块名前缀即可,比如下面这样:

为保险起见,我们可以约定用对应的模块文件所在的文件夹需要保持与模块名相同,然后定义action的文件名(比如action-types.js)也相同,这样我们就可以在现有的前端编译/构建工具中很方便的加入对action-types中命名是否符合规范的检测了,毕竟手动维护都是存在笔误风险的,或者新员工可能并不知道团队有类似这样的命名约定,这些我们都需要加以预防。

浅谈项目中node和依赖的node包的版本管理

这是最近才注意的一个问题。之前在上一家公司的时候,有个项目组的项目,经常发生有的人的电脑里安装项目依赖后无法启动项目的问题,然后每每都是让他从别的同事那里把node_modules文件夹打包拷贝过来,我刚去那个项目组的时候也碰到了这个问题,也是从别的同事那里拷贝了一份node_modules文件夹才能正常跑项目的,后来经过尝试发现好像如果我将npm命令的版本降低到2+再安装项目依赖的话就能正常跑了,但是这个显然是个很奇葩的现象,另外需要说的是他们那个项目也有package-lock.json这个文件的,这个项目因为不是一开始就在跟的项目,我也搞不拎清究竟是哪里出了问题。但是node包的版本管理依旧很重要,不管理的话更容易发生类似这样的问题。

一、不要直接修改package-lock.json/yarn.lock文件

如果要增删项目的依赖或者修改其版本号,不要手动直接去修改package-lock.json/yarn.lock文件,而要通过对应的npm命令或yarn命令进行操作:

1、通过npm增删包

2、通过yarn增删包

通过这些命令去更新项目依赖信息的同事,不仅会自动更新package.json文件,还会同步更新package-lock.json文件(如果用的是npm)或yarn.lock文件(如果用的是yarn)。而且这里有个特点,通过这种方式更新的package.json文件里的项目依赖都是将依赖包按包名字母顺序排列的,这样比较好找包的版本信息。如果你打开一个项目的package.json文件发现里面的包名排列没有顺序乱七八糟的,那就说明这个人是直接自己手改。

二、指定node包到具体的小版本号

因为:

1、你不能信任第三方包都完全按照语义化版本号规则去更新版本号;

2、你不能信任按语义化版本号更新版本号的第三方包在修复bug或添加新功能时不会引入新的bug;

3、第三方包在更新版本号的时候可能会更新它对应的依赖包的版本号,第三方包依赖的第三方包的版本号变动又可能引发上面的问题。

三、不要在同一个项目里同时混用npm和yarn

假设你通过 npm i -S a 命令添加并安装了node包a,并且实际安装的版本是1.0.1,这时候具体的版本信息是记录在package-lock.json文件里的,过了一个月另外一个同事通过 yarn install 命令安装包a时,由于具体的版本信息在yarn.lock问及那种是不存在的,所以完全有可能这位同事实际安装的是1.1.2版本,这样就会造成包的版本管理混乱的问题,反过来也是一样,不再赘述。

四、团队开发时应当同统一node版本号

团队合作开发的项目中,大家各自电脑里的node版本号也需要进行统一,如果不统一,比如甲本地 node --version 为9.2.1,乙本地使用 node --version 为10.0.0,那么可能会有两种问题:

1、如果一个程序的版本遵循语义化版本的规定,那么a.b.c这样的版本号中,a的变动表示的是较大的版本升级(可能会有不向前兼容的新API等)b的变动表示的是较小的、向前兼容的功能更新,c的变动表示的是一些bug修复。但是我们不能保证第三方程序都严格按照这个语义化版本的规定进行版本号的命令,另外也无法确定只是更新了版本号的c部分的是否只是修复了已知bug而没有再次引入新的bug,所以我个人是建议直接把node版本号进行统一,而不是给出一个允许使用的node版本号区间

2、这里还有一个问题,比如乙在本地添加了一个依赖包,该依赖包要求node版本号10.0.0及以上,由于乙本地node版本号为10.0.0所以可以正常安装该依赖了,但是甲本地的node版本号为9.2.1,就无法安装该依赖包了,团队合作开发的项目,我们当然是希望大家都可以在本地正常安装项目依赖并运行项目的。

如果你本地有多个项目分别要求不同的node版本的话,可以使用nvm这个命令行工具。nvm的安装及使用都是非常简单的。

另外需要加一句:推荐使用标记为LTS的版本,这种版本官方会用较长的时间进行维护,所谓LTS,就是Long-Term-Support长期支持的意思。

五、强制约束node版本和使用npm还是yarn来进行依赖管理

我们当然可以只通过文件和口头约定的方式来约束大家都使用相同版本的node,并约束大家都使用npm或者yarn其中一种(不可混用)来进行依赖包的管理,但是在书面/口头约定之外,在代码上再进行一下限制会更稳妥一些。下面是我从vue2官方webpack模版的源码里抄过来并按自己的需求修改了一下的两段代码。

限制使用的node和yarn的版本:check-version.js:

不允许使用npm进行依赖管理(原理是检测项目根目录下是否存在运行npm install时生成的package-lock.json文件,npm-shrinkwrap.json是作向前兼容的,不是很):check-npm.js:

跟check-version.js对应的,需要在package.json文件里添加类似下面这样的代码(这里我认为yarn的版本可以需用限定的太死所以只是限定了一个区间,之所以要求小于2是因为从1都2属于大的版本更新了,现在yarn还没有出2版本,谁知道到时候对应的命令是否还能使用):

check-version.js和check-npm.js两个文件的使用方法很简单,在平时开发和打包命令对应的文件的开头,添加类似下面的代码即可(注意导入后的代码后面需要加一个括号去执行它,因为上面两个文件导出的都是函数):

 

Vue2.5.16源码解读二:从入口文件进行阅读

接上回,我们确认了一个入口文件 src/platforms/web/entry-runtime-with-compiler.js ,将该文件代码折叠如下图所示,方便一览全局:

可以看到这里 import Vue from './runtime/index' 引入了一个 Vue ,然后定义了下 Vue.prototype.$mount 和 Vue.compile ,看Vue源码之前最好是先过一下Vue的官方文档,这里 Vue.prototype.$mount 是一个关在Vue原型链上的属性,所以通过 new Vue() 创建的Vue的实例都会继承这个 $mount 方法,在看这个方法的作用之前,因为看过了官方文档,并且我之前用了两年的Vue了,所以先猜测下这个大概跟Vue组件生命周期里的 mounted 钩子可能有关系,可能是这个 $mount 方法执行完后就会触发组件生命周期里的 mounted 这个钩子里写的程序。现在详细看下相关代码:

好了,这篇文章对入口文件的分析就写到这里,后续我们将以入口文件为线索展开阅读其他部分的代码。打算看一下 Vue.compile = compileToFunctions 这里的 compileToFunctions 方法,以及 import Vue from './runtime/index' 这里导入的 Vue 。

Vue2.5.16源码解读一:确定阅读入口

准备粗略地阅读下Vue,跟网上别人家的源码解读文章不同的是,这个系列是完全以第一次去读代码的视角带大家一起去读的,而非看完源码后以上帝的总结视角来写的系列文章。

要看我们就直接找比较新的版本看,fork vue代码到我们自己的仓库上,clone到本地后,执行 git tag 发现最新的版本是2.5.16,所以在本地新开一个tag-v2.5.16分支方便阅读以及后续commit(如果要commit的话):

执行完,我们的本地项目就已经切换到新的 tag-v2.5.16 分支上了。

拿到一个项目,我们首先看一下 package.json 文件里的 scripts 字段:

mmp,但是英雄无所畏惧,因为我打算只看 build 命令,所以我们的入口文件就是 scripts/build.js 这个文件了。

接下来我们采用调试代码的方式进行阅读。我用的是webstorm这个IDE,先点右上角edit configuration,然后按下图进行配置:

保存后再点右上角的甲壳虫进行debug。

scripts/build.js 文件开头部分内容如下:

我们就将断点打到 if (!fs.existsSync('dist')) { 这一行,然后开始往下一点点走(step over)。

下面就单独对这个文件添加一些注释方便读者阅读:

 

好了,大概的打包流程是这样的,anyway我们是要读源码,所以我们的关注点应该是这里打包时对应的入口文件,从那个文件开始着手阅读代码。

根据 let builds = require('./config').getAllBuilds() 这行代码,我们跳转到 config.js 文件,发现:

因为我们如果直接通过在html里用script标签引用vue.js进行开发的时候,一般都是引用vue.js的,所以这里我们就看这个 web-full-dev 就可以了(它的 dest 字段对应的是 vue.js 文件)。然后我们通过 entry 字段判断我们要看的入口文件为: src/platforms/web/entry-runtime-with-compiler.js 这个文件。

几个eslint插件的介绍

ESLint支持第三方插件。在使用这些第三方插件前,你需要先使用npm来安装它们。要在eslint配置文件中配置这些插件,需要在配置文件中添加一个“plugins”根属性,其值即为由这些插件名(插件名前面的“eslint-plugin-”前缀可以省略)构成的字符串数组。

说明:由于Node里require函数的限制,全局安装的ESLint只能使用全局安装的ESLint插件,项目里安装的ESLint只能使用项目里安装的ESLint插件。

下面是对一些eslint插件的概述:

eslint-plugin-html插件

通过这个插件你可以让eslint去检测html文件script标签里的js代码。使用示例:

注意:在eslint-plugin-html的文档里我们可以看到这么一句话:

Note: by default, when executing the eslint command on a directory, only .js files will be linted. You will have to specify extra extensions with the –ext option. Example: eslint –ext .html,.js src will lint both .html and .js files in the src directory.

然后我们到eslint官方文档里看到了这么一句话:

Specifying File extensions to Lint

Currently the sole method for telling ESLint which file extensions to lint is by specifying a comma separated list of extensions using the –ext command line option. Note this flag only takes effect in conjunction with directories, and will be ignored if used with filenames or glob patterns.

划重点,这里重要的是这句:Note this flag only takes effect in conjunction with directories, and will be ignored if used with filenames or glob patterns。所以,如果你是通过gulp.src把文件流pipe进gulp-eslint的话,只要你gulp.src引进来的文件包括那些.html文件就可以了。

eslint-plugin-import插件

这个插件意在提供对ES6+ import/export语法的支持,有助于防止你写错文件路径或者引用的变量名。使用示例:

或者使用现成的推荐规则:

eslint-plugin-node插件

添加对node的eslint支持。使用示例:

eslint-plugin-promise插件

这个插件意在通过代码风格检测让开发者养成较好地使用promise的方式(最佳实践,best practices)。比如在对promise使用了then之后会要求你加一个catch捕获下异常,当然如果你的方法是直接return返回了这个promise的话则不会要求你马上加catch(因为毕竟当然你可以稍后在其他地方拿到这个promise后再catch)。使用示例:

或者直接使用现成的推荐规则:

eslint-plugin-standard插件

这是一个为Standard Linter而做的补充插件,一共就扩展了4个规则,使用示例如下:

 

Enable jqgrid to do/cancel selecting only selectable items

I had this requirement these days: We have a table with many rows among which some are selectable and some are not selectable, and there is also a selecting-all button which should support do/cancel selecting all selectable rows. But after glancing over the old project, I found no similar ready-made code, so I search for this by Baidu, and found solution like below (modified and beautified):

The above code seems to work well at first attempt. But you will find that you can not undo the selecting-all operation. I cannot use google for several months, therefore I use bing to search for other solutions, and thank goodness, here are the solution:

Haha! I really think the api document for jqgrid plugin is too complicated that it scares me to earnestly read it.

JS原型与原型链

普通对象与函数对象

JS中,对象分为普通对象和函数对象两种,Object和Function是JS自带的函数对象。凡是通过new Function()的创建的对象都是函数对象,其他的都是普通对象。

下面这些是函数对象:

下面这些是普通对象:

原型对象

每当定义一个对象(函数)时,对象中都会包含一些预定义的属性。其中,函数对象会有一个prototype属性,其值就是我们所说的原型对象(普通对象没有prototype,但有__proto__属性;函数对象同时含有prototype和__proto__属性)。注意__proto__这里proto前后分别都是两个下划线,不是一个。

原型对象其实就是普通对象(Function.prototype除外,它是函数对象,但同时它又没有prototype属性)。

原型对象的主要作用是用于继承:

原型链

上面提到原型对象的主要作用是用于继承,其具体的实现就是通过原型链实现的。创建对象(不论是普通对象还是函数对象)时,都有一个叫做__proto__的内置属性,用于指向创建它的函数对象的原型对象(即创建它的函数对象的prototype属性)。

这个由__proto__串起来的直到Object.prototype.__proto__ ==> null对象的链称为原型链。

  • yakima的__proto__属性指向Person.prototype对象;
  • Person.prototype对象的__proto__属性指向Object.prototype对象;
  • Object.prototype对象的__proto__属性指向null对象。

下面有一些比较特别的情况,看完忘掉就可以了,如果是在准备面试,最好别看,别看混了^_^。

Object是函数对象,是通过new Function()创建的,所以Object.__proto__指向Function.prototype:

Function是函数对象,是通过new Function()创建的,所以Function.__proto__指向Function.prototype。本类创建本类。。。大概类似是这么个意思——人是人他妈生的,妖是妖他妈生的。

另外:

constructor

原型对象都有个constructor属性,用来引用它的函数对象。这是一种循环引用。

综合理解

原型和原型链是JS实现继承的一种模型。

对上例的分析:

  • Dog自身没有price属性,沿着Dog.__proto__属性往上找,因为Dog的赋值是通过var Dog = function () {}实现的,所以Dog其实是使用new Function()创建的,所以Dog.__proto__ ==> Function.prototype,Function.prototype.__proto__ ==> Object.prototype,而Object.prototype.__proto__ ==> null。很明显,整条链上都找不到price属性,只能返回undefined;
  • tidy自身没有price属性,沿着tidy.__proto__属性往上找,因为tidy对象是Dog函数对象的实例,所以tidy.__proto__ ==> Dog.prototype ==> Animal,从而tidy.price获取到了Animal.price的值。