模块化简史

JavaScript 中,模块化是一个现代的概念。本文将简略总结模块化在 JavaScript 世界中的演变路径。总之本文不是一个完整的列表,而是简要说明几个范式变化的里程碑。

1. Script标签和作用域

早期的JavaScript都是内联在HTML中的<script>标签中。好一些的会把内容放到单独的js文件中,总之所有的内容都全部在全局作用域 global scope

在任意一个文件中声明的变量都是在全局的 window 对象上,这样就会暴露到完全不想干的脚本里,可能会导致冲突甚至导致错误。可能脚本会无意中修改了另外一个脚本需要用到的变量。

随着Web应用程序的规模变大,也变得更复杂。作用域 scope的概念,和全局作用域 global scope 的危险逐渐被更多开发者理解。立即执行函数表达式(或IIFE)Immediately-invoking function expressions 被发明并很快成为当时的主流。这种方法把整个文件,或者文件中的一部分代码包含到一个立即执行的函数中。JavaScript中的每个函数都有自己的作用域,这样在函数内定义的变量不会隐式的变成全局变量,这不仅避免了外界访问此作用域的变量,又不会污染全局作用域。

以下是立即执行函数表达式的几个版本,每段代码的作用域都是独立的,要想传变量到全局作用域只能通过 wiindow.frrmeIIFE = true 这种写法:

(function() {
 console.log('IIFE using parenthesis')
})()

~function() {
 console.log('IIFE using a bitwise operator')
}()

void function() {
 console.log('IIFE using the void operator')
}()

使用立即执行函数表达式模式,代码库通过暴露一个唯一的组件并绑定到全局 window 对象上。这样就避免了全局命名空间污染。下面的代码片段展示了用这种模式创建的 mathlib 组件,它包含了一个sum方法。如果想为 mathlib 添加更多模块,可以把每个模块放到一个单独的立即执行函数表达式中,将新方法添加到 mathlib 公共接口,而模块中定义的其他变量都是私有的。

void function() {
 window.mathlib = window.mathlib || {}
 window.mathlib.sum = sum

 function sum(...values) {
   return values.reduce((a, b) => a + b, 0)
}
}()

mathlib.sum(1, 2, 3)
// <- 6

这种模式让开发者第一次可以安全的把每个立即执行函数表达式放到一个单独的文件中,让各种 JavaScript 开发工具激增,减轻了网络压力。

这种开发模式的缺点是没没有明确的依赖树,开发者必须精确的按顺序制作组件文件列表,在使用前先加载,而且要递归的加载。

2. RequireJS,AngularJS和依赖注入

在模块系统(比如RequireJS)还有AngularJS的依赖注入机制出现之前,基本上不考虑这个问题。

下面的代码演示了用 RequireJS 的 define 函数定义 mathlib/sum.js 代码库。这样就把函数添加到了全局作用域。define 回调的返回值,就能作为我们模块的公共接口。

define(function() {
 return sum

 function sum(...values) {
   return values.reduce((a, b) => a + b, 0)
}
})

我们可能会有个 mathlib.js 模块聚合了所有需要的功能。上面的例子中,只包含了 mathlib/sum, 但我们可以用同样的方式加载所有需要的依赖项。用数组方式传入依赖项,就会按照顺序得到他们的公共接口,作为参数传入回调中。

define(['mathlib/sum'], function(sum) {
 return { sum }
})

定义好一个库以后,就可以用 require 方法使用了。注意下面代码如何解析依赖关系链。

require(['mathlib'], function(mathlib) {
 mathlib.sum(1, 2, 3)
 // <- 6
})

这就是 RequireJS 和依赖关系树的优势。不管程序是否包含了成百上千的模块,RequireJS都会解析依赖关系树,不需要仔细维护列表。因为已经将依赖关系准确地列在里需要的位置,就不需要再维护长长的组件列表,以及他们的关系了。

复杂的依赖关系问题解决了,更大的好处是,在模块module级别,这种显示的依赖声明让组件component与应用程序其他部分的关系变得更明显,反过来促进了更大程度的模块化。

RequireJS 也不是没有问题,他的整个设计模式都围绕着异步加载模块的能力,部署后会在主要代码执行前瀑布式发出数百个网络请求。需要有个工具提前处理好,才适合部署到生产环境。问题又回来了,还是需要长长的依赖列表。

AngularJS 的依赖注入系统也遇到了相同的问题。但是他们用了一个优雅的解决方案,用字符串解析替代依赖数组,解析函数名来解决依赖关系。但这种机制与压缩工具不兼容,函数名编程单个字母后就不能正确解析了。

在 AngularJS v1 的后期,引入了一个构建任务,把下面的代码:

module.factory('calculator', function(mathlib) {
 // …
})

转换成下面的格式,这样就不怕压缩了,里面明确包含了依赖的列表:

module.factory('calculator', ['mathlib', function(mathlib) {
 // …
}])

毋庸置疑,这个鲜为人知的构建工具,让构建过程更长。再加上过度设计破坏了本不应该被修改的内容,得到的收益几乎可以忽略不计。大多数开发者还是选择去维护一份依赖数组。

3. Node.js和CommonJS的出现

Node.js又诸多创新,其中一个就是 CommonJS 模块系统。利用 Node.js 可以访问文件系统的优势,CommonJS标准更符合传统的模块加载机制。在 CommonJS 中,每个文件都是一个有自己作用域和上下文的模块。使用 require 函数同加载依赖项,就可以走模块的生命周期中随时动态调用。如下代码片段所示:

const mathlib = require('./mathlib')

与 RequireJS 和 AngularJS 类似,CommonJS 依赖关系也由路径名引用。主要区别是 boilerplate 函数和依赖列表都不需要了,并且模块的接口可以赋值给变量,也可以用在 JavaScript 表达式中。

与 RequireJS 和 AngularJS 不同的是 CommonJS 相当严格。RequireJS 和 AngularJS 可以有许多动态定义的模块,而 CommonJS 在文件与模块一一对应。同时,RequireJS 有几种声明模块的方法,AngularJS 有几种 factories、services、providers等,而且它的依赖注入机制与 AngularJS 框架本身紧密耦合。相比之下,并且只有一种声明模块的方式。一个 JavaScript 文件就是一个模块,调用 require 会加载依赖项,赋值给 module.exports 的任何东西都是它的接口。这样可以实现更好的工具,因为工具可以更容易理解 CommonJS 组件系统的层次结构。

最终 Browserify 的出现抹平了 Node.js 服务器和浏览器的 CommonJS 模块的差别。使用 browserify 命令行界面,只需要提供入口模块的路径,就能将难以想象的大量模块打包到一个文件中。终极功能 npm package registry,让 CommonJS 接管了模块加载生态系统。

当然 npm 不仅限于 CommonJS 模块,甚至不限于 JavaScript 包,但是这仍然是它的主要用途。只要敲几下代码,就能在Web应用中获得数千个包(现在总数已经超过50万个,仍在稳步增长中),并且能再 Node.js 服务端和浏览器中复用大部分功能。对其他系统来说这个优势太强大。

4. ES6,import,Babel和Webpack

随着 ES6 在2015年6月开始标准化,并且 Babel 在此之前很久就将 ES6 转换为 ES5,一场新的革命正在迅速逼近。 ES6规范包括 JavaScript 原生的模块系统,通常称为 ECMAScript 模块(ESM)。

ECMAScript 模块很大程度上受到 CommonJS 模块的影响,有静态声明API以及基于 Promise 的动态编程API,如下所示。

import mathlib from './mathlib'
import('./mathlib').then(mathlib => {
 // …
})

ECMAScript 模块中,每个文件都有自己的作用域和上下文。ECMAScript 模块优于 CommonJS 模块的一个主要优点就是它的静态引入依赖关系,可以让第三方工具在运行前得出模块之间的依赖关系,进行代码检查。

静态引入和有选择地引入,使得第三方工具可以有选择地删除未使用的包做优化,这个过程被称为 “tree shaking”

在 Node.js v8.5.0 中也支持了 ECMAScript 模块(需要设置flag)。 大多数现代浏览器也支持 ECMAScript 模块(需要设置flag)。

Webpack 是 Browserify 的继承者,由于具有更广泛的功能,基本上成了通用的模块 bundle 工具。 就像 Babel 和 ES6 一样,Webpack 长期以来一直支持 ECMAScript 模块的importexport 以及动态的 import()函数。 它引入了”code-splitting”机制,它能够将应用程序划分为不同的 bundle 包以提高首次加载体验的性能。

鉴于ECMAScript 模块是该语言的原生实现(对比CommonJS 模块)可以预期在几年内完全接管模块生态系统。

作者:Nicolás Bevacqua
原文:A Brief History of Modularity 发表于2017年9月26日
翻译:https://yukun.im/javascript/815

使用 certbot 给网站设置 let’s encrypt 免费证书

certbot 现在已经智能到只在命令行运行就能配置成功了,不需要手工验证证书,不需要修改nginx配置。以 ubuntu 16.4 + Nginx 为例:

1. 首先安装 CERTBOT

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx 

2. 申请 LET’S ENCRYPT 免费证书

$ sudo certbot 
  • 会列出所有已配置的域名列表,域名前面是序号

  • 根据提示信息,输入用空格或者逗号分隔的序号

  • 如果一切正常,会提示证书已经安装,询问是否修改 nginx 配置文件强制 http 跳转到 https

  • 设置结束,提示你去 ssllabs.com 检测证书是否正常

以上就是设置全部的步骤。

3. 更新

更新命令是

$ sudo certbot renew --post-hook "service nginx reload"

更新完会使用post-hook重新加载 nginx 配置文件。

如果想要自动更新添加到定时任务就可以了:

$crontab -e

加上这一行

00 00 * * 1     certbot renew --post-hook "service nginx reload"

这样每周都会去检查一次

怎样用一天时间,开发上架一个天气小程序

早上醒来,我不愿意回想昨天温度多少度,只想要知道今天比昨天热还是冷,适当增减衣服就行了。穿衣指数什么的根本不适合我,污染指数也没啥用,难道我能不上班嘛?

那么我的需求就是有个天气应用,告诉我今天和昨天天气对比就行了。

历史天气接口不好找,我花了几个小时搜了国内外十几个天气API,很少有历史天气查询,有的也是付费服务。免费天气预报接口倒是很多。

为一个没有几个人用的小程序付费购买接口太奢侈了,这时想到一个绝妙的(笨)方法:查询到今天天气以后,缓存起来,明天再来看就有昨日天气了!

说干就干。

1. 注册小程序

小程序注册入口在这里 https://mp.weixin.qq.com/wxopen/waregister?action=step1 填写基本信息后,验证邮箱和微信,就能登录管理后台了。

在管理后台填写小程序名称、介绍和头像,会自动生成小程序码。
在开发设置页面可以看到AppID(小程序ID),记住这个 AppID。

2. 使用微信开发者工具

微信开发者工具在这里下载 https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html 下载后,用微信扫码打开,创建项目,这需要填入刚才的AppID。


假设你已经知道了微信开发的基础,代码应该有类似的结构。

3. 获取位置信息

(注:和风天气支持经纬度查天气,第3步是可选步骤)

要预报当地天气,需要知道位置,微信小程序有 wx.getLocation 可以获取经纬度。

wx.getLocation({
  type: 'wgs84',
  success: function(res) {
    var latitude = res.latitude
    var longitude = res.longitude
    var speed = res.speed
    var accuracy = res.accuracy
  }
})

但是天气查询的接口很多都不支持经纬度查询。我们需要用经纬度查城市名,这次使用腾讯地图的API。

我们需要的接口就是这个页面 http://lbs.qq.com/qqmap_wx_jssdk/method-reverseGeocoder.html 中的 reverseGeocoder,用经纬度可以获取详细的城市信息,但是我们只需要城市名就可以了。

要使用接口,先要注册腾讯lbs服务的开发者,页面右上角用QQ号登录,然后验证手机号,然后就可以调用接口了。

在小程序中使用前,要在小程序设置界面,开发设置中添加request合法域名: http://apis.map.qq.com 。

// 先在项目中引入微信小程序JavaScript SDK
var QQMapWX = require('../../lib/qqmap-wx-jssdk.min.js');

// onLoad中,实例化API核心类
  qqmapsdk = new QQMapWX({
    key: 'QQ lbs 开发者密钥'
  });


// 在 onShow 中获取城市名
qqmapsdk.reverseGeocoder({
  location: {
    latitude: latitude, // 使用 wx.getLocation 的返回值
    longitude: longitude
  },
  success: function (res) {
    console.log(res);
    if(res.status == 0){
      // 获取到城市名
      let city_name = res.result.ad_info.city;
    }
  },
  fail: function (res) {
    console.log(res);
  },
  complete: function (res) {
  //  console.log(res);
  }
});

4. 查询天气

得到城市名后,再用城市名查询天气的接口,得到未来三天天气预报。

天气接口使用和风天气 https://www.heweather.com/douments/api/s6/weather-forecast 。和风天气的接口比较简洁,返回值也有中文描述可以直接显示。免费版的天气信息足够多。历史天气接口需要付费,我们先用免费的接口。

同上,要使用接口,需要先注册开发者账户,验证手机。

在小程序中使用前,要在小程序设置界面,开发设置中添加request合法域名: http://free-api.heweather.com 。

wx.request({
    url:'https://free-api.heweather.com/s6/weather/forecast',
    data:{
      location:location,
      key: '和风天气开发者密钥',
      rnd:new Date().getTime() // 随机数,防止请求缓存
    },
    success:function(res){
      console.log(res);
    }
  })

拿到天气以后在本地做缓存,最多只存两天的记录。

5. 美化前端界面

对前端程序员来说,设计酷炫的界面有点难,但是基本的审美还是有的。

用关键词 “simple weather app” 在 Google 搜图片,出来的看起来舒服的界面,借用一下配色。

搜索结果中还发现一个可爱的logo,还是免费的!只有一条要求,需要在使用时展示这个网站的链接,因为是小程序,不能外链,我放了文本格式的网站地址,就是这个 https://www.freepik.com/free-vector/simple-weather-app_874144.htm 。


做好的界面。
晚上又优化了一下代码,还在12点前后做了测试,修改了几个问题,就提交审核了。

6. 测试小程序

就算是这么小的项目,测试也必不可少。经过测试发现和风天气的返回值,是未来三天的天气数组,12月7日晚上调用返回的结构与API一样,包含了[12-7,12-8,12-9]的天气。

和风天气接口测试

• 问题1:但是过了午夜12点以后,返回的仍然是[12-7,12-8,12-9],就不能随便的使用 arr[0] 当作今日天气了。

• 问题2:早上起床不到8点,看审核还没通过,再调试一次看看,这次调用返回的数组只有一个天气[12-8],倒是有今天了,明天后天是没有的,好在我现在还不需要。

7. 审核通过

八点又看了一下,上面的API问题不会影响程序。一个小时以后,审核通过了,

审核后,兴奋的发给朋友试用。现在才发现一个重要的问题,如果哪天没打开,第二天就没有昨日天气了,需要每天都打开一次!真希望有免费的历史天气接口啊,哪怕只有简化的昨日天气也行啊。


如果你想试用,可以在微信搜“昨日天气”小程序。如果这个需求很多,可能我会考虑买付费的历史天气接口。

后记

嗯,哪天想起来这个小程序了,还不能用…… 那你比别的天气应用,有什么区别呢。

说起来跟别的应用比较,微信可以直接搜天气,手机也都有天气app,包含昨日天气的也很多,小程序的优势在哪里呢? 看起来打开app的路径,并不比小程序复杂啊!

React-Native版来了 怎样用一天时间,开发发布一个天气App

相关链接

小程序注册
小程序开发工具
腾讯地图lbs服务
和风天气接口
Designed by Freepik

什么是关键渲染路径

这是去年看过 pagespeed insights 以后做的一个简单的分享,重新整理发布。

回顾浏览器渲染过程

  1. 加载HTML
  2. 加载资源
  3. 解析
  4. 渲染

页面HTML结构 (http://jslab.pro/demo/critical_rendering_path/index.html

页面加载过程


(注:紫色是 DomContentLoaded,红色是 Load)

DOM树解析过程,Document Object Model (DOM)

  • Conversion字节码根据编码转换成字符
  • Tokenizing将字符串转化为特殊的标签,如“<html>”、“<body>”
  • Lexing将标签转换成“对象”,定义他们的属性和规则
  • DOM construction根据HTML定义的不同标签的关系(嵌套等),将创建的对象链接到一个树形的数据结构中:HTML对象是Body对象的父级,Body是P的父级等。

最终生成的DOM树

CSSOM 构建过程 CSS Object Model (CSSOM)

页面CSS样式

CSSOM 构建过程

CSSOM

生成渲染树,Render tree

  1. 从DOM树根节点开始,遍历每个可见节点。
  2. 每个可见节点找到并应用对应的CSSOM规则。
  3. 返回计算好样式的内容

Reflow(Gecko) Layout(webkit) 

根据设备的 viewport 计算每个元素该怎样展示。

<meta name="viewport" content="width=device-width,initial-scale=1">

最后一步,绘制页面,Paint

总结解析渲染步骤:

  1. 处理HTML标签构建DOM树。
  2. 处理CSS标签构建CSSOM树。
  3. DOM和CSSOM树被组合形成 Render tree 渲染树(渲染树只包含需要显示在页面的节点)。
  4. Layout(reflow)计算每个对象的确切位置和大小。
  5. 最后是绘制(Paint),把最终的渲染树对象渲染成屏幕像素。

优化关键渲染路径,Critical Rendering Path

  • Critical必需的,关键的
  • Render渲染
  • Path浏览器显示网页(初始视图initial view )所必须的一系列过程。(初始视图也称为首屏 above the fold,用户不需要滚动就能看到的内容)

所以关键渲染路径的定义就是:浏览器显示网页所必须的一系列事件

路径(the Path)

  1. 浏览器下载HTML文件
  2. 浏览器读取HTML,看到有CSS文件、JavaScript文件、图像(解析HTML,遇到CSS资源,加载CSS、解析CSS,继续解析HTML;遇到JavaScript过程相同)
  3. 浏览器开始下载资源
  4. 浏览器决定没有获取CSS和JavaScript的时候不能显示网页
  5. 浏览器下载到CSS文件并读取,并且没有调用其他内容
  6. 浏览器得到JavaScript之前,仍不会展示网页
  7. 浏览器下载JavaScript并读取,并且没有调用其他内容
  8. 浏览器现在决定可以显示网页

这只是一个最简单的网页加载过程,如果页面有很多资源加载过程就会很复杂。

渲染(the Render)

只有很少的资源会阻塞网页渲染,最常见的就是 CSS 和 JavaScript 文件。

浏览器必须把这些资源全部加载并解析完毕才回开始渲染页面。

举个例子:

1,页面有10个CSS文件,浏览器渲染前,会等待这10个CSS文件全部下载、解析完毕。此时浏览器在等待“关键”的步骤时空白页面。

2,页面还有20个JS文件,CSS都下载解析后,也不会开始渲染,还需要等待这20个JS的加载和解析。

(其他还有什么会阻塞?HTML,字体文件等)
(浏览器会逐步解析、逐步渲染,这就是首屏内容可以单独优化的原因)

关键内容(the Critical)(above the fold)(初始视图initial view )

关键内容 = 初始内容 = 首屏

只要保证首屏尽可能快的显示出来,就算页面有1000张图片,100个JavaScript也没有问题。

怎样优化关键渲染路径?

首先找到渲染(首屏)必需的步骤。然后尽可能的:

  • 减少关键的资源;
  • 减少关键的字节数;
  • 减少关键的路径长度。

1,HTML:精简、压缩、缓存
2,CSS:Render blocking CSS,media query 只加载需要的样式,内联CSS

3,Javascript:Parser blocking Javascript,延时执行,async

渲染步骤 DOM->CSSOM->运行(内联)JS->重新构建DOM

外链JS,要先等待下载,执行再继续解析页面。

<script src=”…” async></script> 不阻塞DOM和CSSOM构建

  • Blocking: <script src=”anExteralScript.js”></script>
  • Inline: <script>document.write(“this is an inline script”)</script>
  • Async: <script async src=”anExternalScript.js”></script>

另外:图片不阻塞DOM构建

下面来看一个实际的例子

<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="app.js"></script>
</body>
</html>

 

第一次修改

<script src=”app.js” async></script>

第二次修改

<link href=”style.css” rel=”stylesheet” media=”print”>

不加载或者内联CSS

(PS:实际上,浏览器会提前下载资源,但是执行还是按照定义的顺序)

(PPS:浏览器会逐步解析、逐步渲染,这就是首屏内容可以单独优化的原因)

案例,分析一个网站的关键渲染路径

http://jslab.pro/demo/critical_rendering_path/index.html
http://jslab.pro/demo/critical_rendering_path/async.html

总结

关键渲染路径的定义

浏览器显示网页所必须的一系列事件。

优化方法

  • 减少关键的资源
  • 减少关键的字节数
  • 减少关键的路径长度

附录

下面是收集的一些其他优化方法还有最佳实践。

优化步骤

先测量,找到瓶颈,再优化。

not all requests are made equal
not all bytes are made equal

—— Paul Irish https://www.igvita.com/

优化技巧:

图片——就算页面有1000张图片,只加载关键的(首屏所需的)3张。其他的9997张都可以延迟加载或者滚屏加载(lazy Load)。

JavaScript文件——可以有100个JS文件,但是渲染头部只需要一个,就延迟加载剩下的99个。

CSS文件——可以有多种做法,合并请求、压缩文件、内联首屏样式

其他手段

异步加载HTML、HTML延迟渲染、内联CSS的缓存处理,减少首屏需要加载的字节数,控制首屏所需字节先加载(第一个14KB)

网页速度的度量

网页速度并不是页面展示下载的字节数,也不是整个页面的大小。要关注的是用户的体验,网页最快可用时间,或者首屏开始渲染时间,才是最重要的。

天猫首页源代码——2016年11月16日,首屏以外的DOM节点动态生成

淘宝首页源代码——2016年11月16日,inline关键CSS,其他CSS异步加载

参考链接:

Chrome Dev tools 查看页面加载

https://developers.google.com/web/tools/chrome-devtools/network-performance/resource-loading

学会看Chrome Dev tools 的 timeline

https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/timeline-tool

一个资源的请求过程

https://developers.google.com/web/tools/chrome-devtools/network-performance/understanding-resource-timing

above the fold 定义

https://en.wikipedia.org/wiki/Above_the_fold

https://www.smashingmagazine.com/2009/09/10-useful-usability-findings-and-guidelines/

延迟加载图片

https://varvy.com/pagespeed/defer-images.html

都有谁在阻塞渲染

https://www.keycdn.com/blog/blocking-the-dom/

阻塞渲染的CSS

https://varvy.com/pagespeed/render-blocking-css.html

阻塞渲染的JavaScript

https://varvy.com/pagespeed/render-blocking.html

async的支持情况(IE10+)

http://caniuse.com/#search=async

网络字体阻塞渲染

http://ianfeather.co.uk/web-fonts-and-the-critical-path/

优化首屏显示

https://varvy.com/pagespeed/prioritize-visible-content.html

First RTT 14KB

https://docs.google.com/presentation/d/1MtDBNTH1g7CZzhwlJ1raEJagA8qM3uoV7ta6i66bO2M/present?slide=id.g3eb97ca8f_10

speed index 速度指标

https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index

https://gtmetrix.com/

谷歌的 pagespeed insights 对图片有损压缩

http://stackoverflow.com/questions/41313719/google-pagespeed-insights-optimize-images-running-new-image-compression

http://stackoverflow.com/questions/5451597/how-does-googles-page-speed-lossless-image-compression-work