2019年了,高手们现在怎么写JavaScript代码

2019年了,高手们现在都是怎么写JavaScript代码的,有没有一些看了就能用上的技巧,近些年有没有什么新的最佳实践?本文就是应StackOverFlow邀请所做的分享,作者慷慨分享了自己使用的方法和经验。读者可以根据实际情况自己选择,就算不接受也算开阔一下思路。原文评论争论比较激烈,也可以进去看看其他人的观点。

作者:Ryland Goldstein
原文链接: Practical Ways to Write Better JavaScript

有什么切实有效的方法写出更好的 JavaScript ,我看谈论这个话题的人不多,本文就是我最常用的一些实践。

1. 使用 TypeScript

改善JS的第一件事就是不要写JS。对不了解的人说明一下,TypeScript(TS) 是JS的严格超集(现有的JS程序都是合法的TS程序)。TypeScript 为原生 JavaScript 加上了(可选的)全面的类型批注。很长一段时间里,TS的支持不是很好,我也不推荐使用。但是现在大多数框架都支持了。知道了TS是什么,我们来看看为什么要用它。

1.1 TypeScript 强制类型安全

类型安全是这样实现的,编译时检查代码中所有的类型都以合法的方式使用。比如你创建了一个函数 foo 接受一个数字类型参数:

function foo(someNum: number): number {
  return someNum + 5;
}

调用这个 foo 函数时,只能传入数字:

// 正确
console.log(foo(2)); // 7
// 错误
console .log(foo("two")); // 不合法的 TS 代码

除了为代码添加类型的工作量,强制类型安全没有任何副作用。但是这样做的收益不容忽视。类型安全能防止一些常见的错误或者bug,这对弱语法的JS来说是一件幸事。

1.2 TypeScript 让大型应用得以重构

重构大型JS应用真的是一场噩梦,过程中最大的痛苦来自于没有强制参数类型检查,也就是说JS函数一点容错性都没有,假设我有一个函数 myAPI 被一千个服务调用:

function myAPI(someNum, someString) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}

这时候我把参数改成这样:

function myAPI(someString, someNum) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}

我必须保证这一千个函数调用,都要正确地修改参数,漏掉一个,程序就会出错。而在TS中是这样的:

// before
function myAPITS(someNum: number, someString: string) { ... }
// after
function myAPITS(someString: string, someNum: number) { ... }

如你所见,函数 myAPITS 做了与JS一样的修改,但在JS中不会报错的代码,在TS中会通不过编译,所有调用的地方都会报出类型错误。及早发现错误,而不会有bug隐藏在代码中。

1.3 TypeScript 让团队架构沟通更容易

正确设置 TS 后,不先定义接口和类型就很难开始写代码。这就有了一种简洁、易沟通的架构方案。在TS之前,不是没有类似方案,但是都需要做额外的事来达到同样的效果。用TS后,我可以把下面的提案发给同事:

interface BasicRequest {
  body: Buffer;
  headers: { [header: string]: string | string[] | undefined; };
  secret: Shhh;
}

我要先写一点代码,但是我可以不用花太多时间就能让同事明白我的进度,并获得反馈。我不确定TS本质上是不是比JS更不容易出错,但是我坚信强制开发者先定义接口能写出更好的代码。开发者肯定更熟悉原生JS,但是我现在做的大多数新项目,从一开始就是用TS。

2. 使用新特性

JavaScript 是世界上最流行的编程语言(写不写之一都行),这是一个数以亿计的人使用过的,超过20年历史的语言。最近很多修改和新特性加到了JS中(严格来说叫ECMAScript),已经从根本上改变了开发者的体验。作为一个在两年前才开始用JS的人,我的优势是没有偏见和历史包袱。能让我选用或者放弃某些功能,做出更加务实的选择。

2.1 async 和 await

在很长一段时间,异步、事件驱动的回调是JS开发中不可避免的一部分:

// 传统的回调
makeHttpRequest('google.com', function (err, result) {
  if (err) {
    console.log('Oh boy, an error');
  } else {
    console.log(result);
  }
});

我不在这里花时间解释上述代码的问题(但是我以前解释过)。为了解决回调问题,JS中新增了 Promise 的概念,使用 Promise 可以不用嵌套就能写出异步逻辑。

// 使用 Promise
makeHttpRequest('google.com').then(function (result) {
  console.log(result);
}).catch(function (err) {
  console.log('Oh boy, an error');
});

Promise 最大的优点在于可读性和链式写法,虽然很棒,但Promise写法还是太像以前的回调。那么有没有一种更好的替代方法呢,ECMAScript委员会决定添加一种新的方法来使用promises、async、wait:

// async 和 await
try {
  const result = await makeHttpRequest('google.com');
  console.log(result);
} catch (err) {
  console.log('Oh boy, an error');
}

唯一需要注意的是使用 await 之前,函数要定义为 async

// 上面的代码要这样定义 makeHttpRequest 函数
async function makeHttpRequest(url) {
  // ...
}

也可以直接 await 一个 Promise ,因为实际上 async 就是一个包装后的 Promise,所以请随意使用 async/await 不必感到内疚,他们与 Promise 是等价的。

2.2 let 和 const

一直以来,JS中声明变量只有一个范围会变化的限定符: var。它的作用范围规则很独特很有趣,var 的作用域不一致,不明确,可能导致意外发生,也是很多bug的元凶。但是在 ES6 中有了替代方法:let 和 const。这就没必要再用var了。所以以后不要使用 var ,任何用 var 的地方都可以转换为等效的 constlet 代码。

至于何时使用 const 何时使用 let,我都是先用 const , const 强制性更强,不可变,写出的代码更好。并没有太多“真正的场景”需要用到 let,可以说我代码里 let 用的不到 const 的 1/20 。

这里说的“不可变”(immutablish)不是C/C++中的 const 。JS中用 const 声明的变量不是说永远不会改变,而是指向变量的地址不变。简单类型(number、booolean等)内容永远不会变,但是所有对象(class、array、dict),const不保证不改变。

2.3 箭头 => 函数

剪头函数是JS中一种更简洁的声明匿名函数方法。匿名函数就是未明确命名的函数,一般用在回调或者事件处理函数。

// 匿名函数的普通写法
someMethod(1, function () { // has no name
  console.log('called');
});

一般情况下,这种写法不算错,但是匿名函数的作用域很“有趣”,可能会导致意料之外的错误。有了箭头函数,再也不用担心作用域的问题了,下面是用箭头函数实现同样的功能:

// 用箭头定义的匿名函数
someMethod(1, () => { // has no name
  console.log('called');
});

除了更简洁,箭头函数的作用域更实用,箭头函数的 this 继承自函数定义时的作用域(译注:而不是使用时所在的作用域)。

有时候,箭头函数还可以写的更简洁:

const added = [0, 1, 2, 3, 4].map((item) => item + 1);
console.log(added) // prints "[1, 2, 3, 4, 5]"

单行的箭头函数有个隐式 return 语句,也不用写前面的括号和函数体的分号。

再多说一点,用 var 声明变量的时候可不能这么用(译注:此处是说单行箭头函数返回对象字面量行不通)。普通的匿名函数还是有他的应用场景,默认用箭头函数,比默认用普通匿名函数能节省不少调试时间。

再次声明 MDN文档真好用。

2.4 扩展运算符 …

提取一个对象的键值对放到另外一个对象是常见的场景。以前有好几种方法实现,但是看起来都有些笨拙:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
const merged = Object.assign({}, obj1, obj2);
console.log(merged) // prints { dog: 'woof', cat: 'meow' }

这种写法很常见,也很乏味,多亏了扩展运算符,再也不用像上面那样写了:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
console.log({ ...obj1, ...obj2 }); // prints { dog: 'woof', cat: 'meow' }

更棒的是,数组也可以(译注:但是不能混用):

const arr1 = [1, 2];
const arr2 = [3, 4];
console.log([ ...arr1, ...arr2 ]); // prints [1, 2, 3, 4]

2.5 模板字符串

在很多语言中,字符串都是最常见的结构之一,所以内置的字符串都做不好也太尴尬了,JS就属于这种语言。有了模板字符串以后,JS变得别具一格,解决了字符串处理中两个大问题,加入变量和换行:

const name = 'Ryland';
const helloString =
`Hello
 ${name}`;

代码可以说明一切,是不是很棒。

2.6 对象解构

对象解构可以不遍历数据或者访问对象的 key 就能获取值的方法,对象、数组等(译注:有 Iterator 接口的)都可以使用。

// 以前的写法
function animalParty(dogSound, catSound) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

animalParty(myDict.dog, myDict.cat);

对象解构:

function animalParty(dogSound, catSound) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

const { dog, cat } = myDict; // 对象解构
animalParty(dog, cat);

还可以在函数参数解构:

function animalParty({ dog, cat }) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

animalParty(myDict);

数组的例子:

[a, b] = [10, 20];

console.log(a); // prints 10

还有很多新增的现代特性,下面几个我也特别推荐:

3. 始终假设你的系统是分布式的

写并行程序时,你从一开始就会优化,如果一核工作三核围观,就浪费了75%的算力。阻塞式的同步操作,是并行计算的最大敌人。但我们说的是JS,这是一个单线程语言,讨论这个有意义吗?

JS是单线程但不是单文件,没有并行计算,但是有并发。一个HTTP请求可能需要几秒钟甚至几分钟,这时候如果停下来等待请求返回,页面就没法用了。

JavaScript用事件循环解决了这个问题。事件循环遍历已注册的事件,根据JS内部调度和优先级逻辑执行这些事件。这就可以同时发送数千个(译注:浏览器限制一般几个或十几个)HTTP请求,也可以同时加载多个文件。要注意的是:只有正确的使用特性才能利用这一功能,举一个简单的例子, for 循环:

let sum = 0;
const myArray = [1, 2, 3, 4, 5, ... 99, 100];
for (let i = 0; i < myArray.length; i += 1) {
  sum += myArray[i];
}

原生的 for 循环是最难并行的构造之一。让for循环并行执行最大的困难源于一些特殊的代码结构,这些结构很少见,但是只要存在,for 循环就很难并行。

let runningTotal = 0;
for (let i = 0; i < myArray.length; i += 1) {
  if (i === 50 && runningTotal > 50) {
    runningTotal = 0;
  }
  runningTotal += Math.random() + runningTotal;
}

上面代码只有按顺序迭代执行,才能产生预期结果,如果试图让多个迭代并行,可能就会进入不正确的分支,导致计算结果无效。建议仅在绝对有必要时才使用传统的for循环,平时可以用以下代码代替:

// map
// in decreasing relevancy :0
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
const resultingPromises = urls.map((url) => makHttpRequest(url));
const results = await Promise.all(resultingPromises);

//map with index
// in decreasing relevancy :0
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
const resultingPromises = urls.map((url, index) => makHttpRequest(url, index));
const results = await Promise.all(resultingPromises);

//for-each
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
// note this is non blocking
urls.forEach(async (url) => {
  try {
    await makHttpRequest(url);
  } catch (err) {
    console.log(`${err} bad practice`);
  }
});

我解释一下,为什么这些写法是对传统for循环的改进,map 并不是以顺序迭代,而是将每个元素分别提交给用户定义的事件处理函数。大多数情况下,各个迭代之间没有依赖关系,因此可以并行。也不是for循环不能做同样的事,可以这样写:

const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

async function testCall() {
  // do async stuff here
}

for (let i = 0; i < 10; i += 1) {
  testCall();
}

是的for循环也可以,但是没有map简单,来看 map 版本:

const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
items.map(async (item) => {
 // do async stuff here
});

map这样写就可以了,用map还有一个好处就是如果你想等待所有异步操作完成后做处理,也可以很简单的实现,如果是用for循环的话,就需要自己管理一个数组才行,来看map怎么做:

const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const allResults = await Promise.all(items.map(async (item) => {
  // do async stuff here
}));

简单吧。

许多情况下,for循环与map和forEach的性能相当,可能更好一些,我仍然认为牺牲几个循环的时间,换来更清晰的API完全值得。未来也有机会改进请求数据的部分,而不用操心循环部分。for循环没有区分各种情况,要优化就麻烦的多了。

map和forEach之外还有其他可用的异步选项,例如for-await-of。

Lint和格式化

代码没有统一的风格,就很难阅读理解。因此高端的代码不管用任何语言写,都需要有统一并且合理的风格。JS语言生态系统广泛,代码格式和指南有很多种,一定要选择一种来格式化你的代码。选不选比选择哪个重要的多。

我看到很多人问到底是该用 eslint 还是 prettier。在我看来他们的用途不同,需要结合使用。Eslint 是一个老牌的lint工具,比起关注样式,更多的注重正确性。我在用AirBNB的Lint规则的配置,使用这个配置的时候以下的语句将会Lint失败:

var fooVar = 3; // airbnb 规则禁止使用 "var"

eslint 对开发的好处显而易见。从本质上说,它确保你的代码遵循最佳实践,有些规则不一定全部适合,可根据实际情况定制。

Prettier 是代码格式化工具,不太关心正确性,更注格式统一。 Prettier 不会抱怨你有没有使用 var,但是会自动对齐代码中的所有括号。我自己的开发过程中,推送代码到 git 之前总会跑一遍 Prettier 把代码格式化。还可以在每次提交git时自动格式化代码,保证入库的代码都有一致的风格。

4. 测试代码

写测试改善JS代码,是一种间接但是非常有效的方法。JS生态系统中有很多成熟的测试工具,没有一个工具可以处理所有的情况,我建议你熟悉各种测试工具,根据需求选择使用。

4.1 测试驱动程序 – Ava

Github上的Avajs

测试驱动程序,简单来说就是一个框架,在高层次提供结构和实用工具,根据测试需求不同,常常与其他特定的测试工具一起使用。

Ava 在功能和简洁之间取得很好的平衡。它的异步特性和并发运行架构是我最喜欢的。更快的运行节约了开发者的时间也为公司省钱。Ava保持简洁的同时,还内置了一些很不错的特性,比如断言。

替代选择:Jest,Mocha,Jasmin。

4.2 Spies 和 Stubs – Sinon

Github 上的 Sinon

spies 是一个函数分析工具,比如一个函数被调用了多少次,每次调用的详情等数据。

Sinon 可以做很多事,有几个工具超级好用, spiesstub 是最出色的两个,功能丰富而且语法简洁,

替代选择: testdouble

4.3 模拟请求 – Nock

github上的nock

HTTP模拟就是拼装http请求,模拟服务器端行为,结合自己的代码方便测试过程。

HTTP模拟写起来可能很麻烦,但是 nock 让这个过程方便不少,它直接重写 nodejs 的内置请求,并拦截传出的请求,让你可以完全控制响应。

替代选择:我没听说有。

4.4 Web 自动化测试 – Selenium

Github上的 Selenium

推荐 Selenium 让我心情复杂,由于它是Web自动化最流行的选择,拥有庞大的社区和在线资源。不幸的是,学习曲线相当陡峭,而且也依赖了太多的外部代码库。话虽如此,它也是唯一真正免费的选择,因此除非你是在进行企业级的Web自动化,否则 Selenium 都可以胜任。

替代选择:Cypress,,PhantomJS

永无止境的旅程

跟世间大多数事情一样,写出更好的JavaScript是一个持续的过程,代码总是可以更干净,总会有新特性被添加,测试怎么也不嫌多,让人应接不暇疲于应付,但是有这么多可以改进的地方,如果按照自己的步伐,一步一个脚印的做,不知不觉中就会成为JavaScript高手。

云开发·云调用生成小程序码

小程序云开发已经支持云调用,开放了很多接口,一直想要的获取小程序码也支持了。这下轻量的小程序也可以有自定义小程序码的功能。

1. 需求

获得一个带参数的小程序码,传播出去以后,用户扫码进入指定页面,根据参数做不同的处理。本文只讲小程序码生成、存储、展示部分。参数处理不多介绍,可以查看 项目代码 了解更多。

2. 开通云开发

新建小程序可以从开发工具的云开发模板初始化项目,根据云开发操作指引新建项目即可。

但是这里有个问题,已发布小程序的页面才能生成小程序码。如果现有的小程序没有开通云开发,需要做以下几步:

  1. 开发工具开通云开发,设定云开发的环境;
  2. 将原来的代码(除了project.config.json以外的所有文件)放到新建的 miniprogram 目录;
  3. 新增 cloudfunctions 目录;
  4. app.json 新增配置 "cloud": true
  5. project.config.json 配置 "miniprogramRoot":"miniprogram/""cloudfunctionRoot":"cloudfunctions/"
  6. 修改小程序基础库版本,最低要 2.3.0 "libVersion": "2.3.0"

3. 生成小程序码

下面可以开始写代码开发了,开始之前,建议先看完官方教程。特别是开发工具的使用步骤,开发和调试时如果遇到奇怪的问题,可以尝试重启开发工具、重装开发工具,也可以去微信开放社区发帖。(重启和重装都是我在社区中发现的答案,能解决各种不应该存在的问题)。

3.1 准备文件

cloudfunctions目录右键新建Node.js云函数 getqr

生成小程序码需要单独指定权限。在 getqr 目录新建 config.json ,里面写以下内容:

{
  "permissions": {
    "openapi": [
      "wxacode.getUnlimited"
    ]
  }
}

小程序码的获取方式有三种,这里只用到了接口 getUnlimited,选择这个接口的原因是漂亮的圆形小程序码,数量无限制。具体区别可以去 获取小程序码官方文档查看详情。

正常情况下,这个时候云函数可以部署测试了。如果遇到部署不成功、各种权限问题,可以尝试本地部署上传所有文件、重启试试。

3.2 生成小程序码

生成小程序码的代码如下,可以指定页面和页面参数 scene,还有小程序码的尺寸。

注意这里的 scene 有限制:
1. 最大32个可见字符;
2. 只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~
3. 注意参数格式:下面实例代码生成小程序码后,扫码获得 pages/demo/demo?scene=id%3D6

try {
  const result = await cloud.openapi.wxacode.getUnlimited({
    page: 'pages/demo/demo',
    scene: 'id=6',
    width: 240,
  })
  console.log(result)
  return result
} catch (err) {
  console.log(err)
  return err
}

直接调用,比服务端调用少了 access_token 参数。

3.3 上传到云存储

返回值中的 buffer 就是图片内容,直接上传到云存储:

const uploadResult = await cloud.uploadFile({
  cloudPath: 'shareqr/' + qr_name_hash + '.jpg',
  fileContent: result.buffer,
});
  • 我在云存储新建了 shareqr 目录保存小程序码;
  • 图片名根据参数取md5摘要;
  • getUnlimited 返回的图像是 jpeg 格式,后缀硬编码写 .jpg

3.4 获取图片临时路径

直接上代码

getURLReault = await cloud.getTempFileURL({
  fileList: [uploadResult.fileID]
});
fileObj = getURLReault.fileList[0]
return fileObj

3.5 直接从存云存储获取

生成过以后图片已经保存在云存储,用同样的参数第二次调用没必要再生成一次,去掉一次网络请求,可以节省不少时间。

前面说到文件名使用请求参数摘要,知道了目录和文件名,再加上文件bucket前缀就可以拼出来 fileID,用fileID 可以查询云存储的文件。

比如我刚刚生成的 fileID 是 cloud://dev-xxxx.8888-dev-xxxx/qr/44ea42f05091c3bec771123e6e8cd4c2.jpg, 前缀就是 cloud://dev-xxxx.8888-dev-xxxx/。再拼上目录、文件名、后缀就是 fileID

注:此处的 fileID拼接方法并不是来自官方文档,只是在使用中发现这个前缀不会变。还需要官方解释说明fileID规则。
如果会改变,就需要再用云数据库存储fileID,更麻烦一些。

3.6 云函数完整代码

// 云函数入口文件
const cloud = require('wx-server-sdk');
const crypto = require('crypto');
const bucketPrefix = 'cloud://dev-xxxx.8888-idc-4d11a4-1257831628/qr/'; // env: 'dev-xxxx'

// 云函数入口函数
exports.main = async (event, context) => {
  const full_path = event.page + '?' + event.scene;
  const qr_name_hash = crypto.createHash('md5').update(full_path).digest('hex');
  const temp_id = bucketPrefix + qr_name_hash + '.jpg';
  // return {
  //   full_path,
  //   qr_name_hash,
  //   temp_id
  // }

  try {
    // 先尝试获取文件,存在就直接返回临时路径
    let getURLReault = await cloud.getTempFileURL({
      fileList: [temp_id]
    });
    // return getURLReault;
    let fileObj = getURLReault.fileList[0];
    if (fileObj.tempFileURL != '') {
      fileObj.fromCache = true;
      return fileObj;
    }

    // 生成小程序码
    const wxacodeResult = await cloud.openapi.wxacode.getUnlimited({
      scene: event.scene,
      page: event.page,
      width: 240
    })
    // return wxacodeResult;
    if (wxacodeResult.errCode != 0) {
      // 生成二维码失败,返回错误信息
      return wxacodeResult;
    }

    // 上传到云存储
    const uploadResult = await cloud.uploadFile({
      cloudPath: 'qr/' + qr_name_hash + '.jpg',
      fileContent: wxacodeResult.buffer,
    });
    // return uploadResult;
    if (!uploadResult.fileID) {
      //上传失败,返回错误信息
      return uploadResult;
    }

    // 获取图片临时路径
    getURLReault = await cloud.getTempFileURL({
      fileList: [uploadResult.fileID]
    });
    fileObj = getURLReault.fileList[0];
    fileObj.fromCache = false;

    // 上传成功,获取文件临时url,返回临时路径的查询结果
    return fileObj;

  } catch (err) {
    return err
  }

}

4. 小程序页面调用

调用页面就比较简单了,在小程序新建一个 pages/share/shareonLoad 函数调用云函数。

// 使用前记得先初始化云函数,一版放到 app.js onLaunch() 中
// wx.cloud.init({env: 'dev-8888'})

wx.cloud.callFunction({
  name: 'getqr',
  data: {
    page: 'pages/demo/demo',
    scene: 'id=6',
  }
}).then(res => {
  console.log(res.result);
  if (res.result.status == 0) {
    _this.setData({
      qr_url: res.result.tempFileURL
    })
  }else{
    wx.showToast({
      icon: 'none',
      title: '调用失败',
    })
  }
}).catch(err => {
  console.error(err);
  wx.showToast({
    icon: 'none',
    title: '调用失败',
  })
})

至此完整的调用过程已经全部完成,详细代码可以到 项目代码 查看。

代码中还对入口页面和share页面的参数做了包装,云函数可以直接使用,小程序可以稍做修改适应自己业务。

写在最后

小程序云开发已经开放了很多功能,除了这次提到的生成小程序码,云调用还可以发送模板消息。有需要的开发者又一个理由可以快速上线新功能了。

云开发还开放了HTTP API,也就是用自己的服务器调用云函数。以前看完云开发介绍文章最大的疑问就是,你说的都很好,可是后台数据怎么管理呢?不能跟自己的服务器结合,只能放一些轻量的小程序。有了 HTTP API 以后就可以用自己的服务器做管理后台了。这时候你要问,都用上服务器了,还需要云开发做什么。首先,云开发免费;其次,免费功能已经够强,就差不能做Web管理后台了;最后,获取access_token(小程序及小游戏调用不要求IP地址在白名单内。)

模块化简史

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