红颜漠 - 专注前端,记录生活

得不到的,才是最想要的

0%

因项目的需求,自己动手写了一个 PostCSS 插件 postcss-px2vw,主要用于将 px 转成 vwremrem 作为回退模式。也刚好借此机会总结一下 npm 包的发布流程,文章还会介绍到七牛云图片的使用与上传相关的技巧,以及期间遇到的一些问题。

为什么需要它

转换 px 单位的插件有很多,知名的有 postcss-px-to-viewportpostcss-pxtorem,前者是将 px 转成 vw,后者是将 px 转成 rem

起初是看了大漠的一篇文章《如何在Vue项目中使用vw实现移动端适配》,于是怀着激动的心情,就在项目中也使用 vw 来做移动端的适配。该文章大力推行用 vw 代替 rem 做适配,在amfe-flexible 项目文档中也推荐 vw 的替代方案。但是考虑到移动端对 vw 的支持情况不如 rem,所以仍有很多项目都选择使用 rem 来布局。于是就想到将 rem 作为一种回退机制,或许觉得没必要,直接放弃 vw 使用 rem 不就完了?确实,不过既然是折腾,也就不需要那么多理由了,其实饿了么平台就用了此方案。

关于移动端适配方案,也有一些个人的亲身体会,有时间另启一篇文章详细总结一下

实现方案

首先,得提一下 CSS 样式的回退原理:当 CSS 遇到无法识别的一些样式时,不会报错,而是忽略它。并且后面的样式声明比前面的样式声明权重要高(也就是会覆盖前面的样式)。所以,我们要达到的效果是这样的:

1
2
3
4
.class {
margin-top: 2rem;
margin-top: 20vw;
}

浏览器会优先使用第二个 margin-top: 20vw;,如果不支持该声明就会直接忽略它,并使用第一个 margin-top: 2rem;

要实现这样的效果,一开始想到的是同时使用上面介绍到的两款插件,设置 postcss-pxtorem 的参数 replace: false,最后的结果是这样的:

1
2
3
4
.class {
margin-top: 20vw;
margin-top: 2rem;
}

虽然最后结果同时保留了两种单位,但是优先使用的是 rem,这并不能满足我们的需求。原因也很简单,看 postcss-pxtorem 源码就会知道,设置 replace: false 时,它会在当前样式下面插入转换后的结果。而 postcss-px-to-viewport 是不能设置 replace 的,所以,无论如何配置,rem 单位始终会在 vw 的下面。

基于这个点,就可以编写我们自己的代码了。

代码实现

打开 postcss-px-to-viewport 源码,其实也就短短的几十行代码。就算没写过 PostCSS 插件,看到源码,依葫芦画瓢,也能写一个适合自己的插件来。这是修改过后的源码:

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
'use strict';

var postcss = require('postcss');
var objectAssign = require('object-assign');

module.exports = postcss.plugin('postcss-px2vw', function (options) {
var opts = objectAssign({
viewportWidth: 750,
unitPrecision: 5,
rootValue: 75,
minPixelValue: 1
}, options);

var pxRegex = /"[^"]+"|'[^']+'|url\([^\)]+\)|(\d*\.?\d+)px/ig;

return function (css) {
css.walkDecls(function (decl, i) {
if (decl.value.indexOf('px') === -1) return;
var value = decl.value;
if (opts.viewportWidth) {
var pxReplaceForVw = createPxReplace(opts.viewportWidth / 100, opts.minPixelValue, opts.unitPrecision, 'vw');
decl.value = value.replace(pxRegex, pxReplaceForVw);
}
if (opts.rootValue) {
var pxReplaceForRem = createPxReplace(opts.rootValue, opts.minPixelValue, opts.unitPrecision, 'rem');
if (opts.viewportWidth) {
decl.parent.insertBefore(i, decl.clone({
value: value.replace(pxRegex, pxReplaceForRem)
}));
} else {
decl.value = value.replace(pxRegex, pxReplaceForRem);
}
}
});
};
});

function createPxReplace(perRatio, minPixelValue, unitPrecision, unit) {
return function (m, $1) {
if (!$1) return m;
var pixels = parseFloat($1);
if (pixels <= minPixelValue) return m;
return toFixed((pixels / perRatio), unitPrecision) + unit;
};
}

function toFixed(number, precision) {
var multiplier = Math.pow(10, precision + 1);
var wholeNumber = Math.floor(number * multiplier);
return Math.round(wholeNumber / 10) * 10 / multiplier;
}

合并了两款插件的核心转换功能,并去掉了一些不常用的功能和配置项,实现最初的目的就行,尽量保持简单易用的原则。该插件只保留了 4 个可配置参数(viewportWidthrootValueunitPrecisionminPixelValue),具体使用说明可查看 项目的README.md 文档。

发布到 npm 仓库

本来只是为了项目的需要,没打算从项目中独立出现来的。一方面为了熟悉一下 npm 的发布流程,另一方面也是为了在自己的 github 仓库中保留一份记录。

整个流程很简单,也就几步:

  1. 要发布到 npm 仓库,首先得新建一个项目。运行 npm init,填写相关参数说明;
  2. 登录 npm 账号:npm login,输入用户名密码以及邮箱。如果没有账号就去 npm官网 注册一个;
  3. 发布:npm publish,大功告成。

不过现实往往没有理论那么容易,总会遇到一些问题。

  1. 如果你使用的是淘宝源,需要先切回官方源,使用 nrm 的话,只需 nrm use npm 即可;
  2. package.json 中的 name 不能与仓库中已有的项目重名。如果难以命名,可添加用户名前缀,如:@moohng/postcss-px2vw。添加用户前缀是不能直接 publish 到公有仓库中的,需要使用 npm publish --access=public
  3. 每次更新发布必须增加 package.json 中的版本号。

七牛云的使用

为什么会使用到七牛云?这本是八竿子也打不着的东西。其实,也就是为了学别人在项目的 README.md 文档最后贴一张个人的收款二维码。先别想其他,就贴一张图本身而言,其实很简单,就是在 markdown 语法中插入一张图片链接地址。

为了一张图片的链接地址,就把七牛云也整一遍?的确,就是出于这个目的于是就研究了一遍七牛云这东西。简单的上传无外乎就是登陆到七牛云平台,然后拽一张图片上去,链接地址就有了。可是,作为一个高大上的程序猿,怎能如此将就,当然得有一套不同寻常的方式了。

最终要达到的效果就是得到一张上传图片的链接地址:http://static.moohng.com/FrEihC8JSWMtsxtnDUpQiuaL9ZbE,自定义域名 + 图片hash值。

域名配置

要配置自定义域名,首先得拥有一个已经备案过的域名。然后在七牛云的融合CDN菜单下可以添加域名:

img

接下来在域名管理平台配置 CNAME,CNAME 值可在刚添加的域名下查看。如此一来,以后访问七牛云上的资源就可以直接通过自定义域名来访问了。

图片上传

获取上传 token

实现上传最关键的就是要有一个 token,token 的生成在官方文档中都有说明。不过我们不用自己写算法,官网已经用多种语言实现了封装。前端可参考Node.js SDK,简单实现:

新建一个项目 demo,初始化 npm init,然后安装七牛云SDK npm install qiniu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// index.js
var qiniu = require('qiniu');

// 可在七牛云查看
var accessKey = 'accessKey';
var secretKey = 'secretKey';

var mac = new qiniu.auth.digest.Mac(accessKey, secretKey);

var options = {
scope: 'moohng', // 存储空间名称
};

var putPolicy = new qiniu.rs.PutPolicy(options);
var uploadToken = putPolicy.uploadToken(mac);

console.log(uploadToken);

其中,accessKeysecretKey 可在七牛云个人中心查看。

在 Nodejs 环境下运行代码,node index.js,得到 token。

上传

上传地址可在七牛云开发者中心查看,不同地区的地址不一样。例如,华南地区:http(s)://upload-z2.qiniup.com。

本来是想写一个图形化界面,用来上传图片。无赖自己比较懒,既然拿到了最关键的 token,也有了上传地址,至于图形化界面有时间再说吧。

作为程序员,逼格最高的莫过于命令行了。于是,就想到了 Linux 下常用的 curl 命令。

1
$ curl http://upload-z2.qiniup.com -F "file=@./img_example.png" -F "token=这里是生成的token"

然后一个优雅的回车,一切就结束了!不出意外会返回一个 key,这个 key 拼接上之前自定义的域名,就可以为所欲为了。

关于 curl 命令,-F 表示上传文件,文件参数与其他参数必须分开写,文件参数使用 @+文件路径。

更多的使用说明及相关参数请参考七牛云开发文档

图片压缩

七牛云提供了多个图片处理工具,裁剪、缩放、压缩等。如果放到 github 上的图片太大,可能会加载不出来,所以,对图片进行缩放和压缩是很有必要的。

处理之前:http://static.moohng.com/FrEihC8JSWMtsxtnDUpQiuaL9ZbE (85.1k)

图片处理之前

处理之后:http://static.moohng.com/FrEihC8JSWMtsxtnDUpQiuaL9ZbE?imageView2/1/w/320/h/320/q/75|imageslim (17.1k)

图片处理之后

最后

关注我,不定期更新前端技术型文章。

先附上项目的链接地址:

github: https://github.com/moohng/validator

动机

有表单的地方必有校验,我们使用不同的框架有不同的校验方法。在PC端基于ReactAnt Design框架中的Form表单的功能就十分强大,而在移动端,却很难找到一款能与之匹敌完整框架。移动端比较流行的vux框架,虽然有的组件自带了校验功能,但然并卵,基本上很难匹配真实项目的需求。

放弃vux组件自带的校验功能,后来在github上找到一款比较流行的校验库vee-validate。这个库做到了与表单组件之间的解耦,确实也能够灵活的实现各式各样的项目需求,但给我的感觉就是有点重。一般来说,移动端的开销越低越好,代码越轻量越好,区区一个校验,实在不忍心用上如此庞大的工具。

思前想后,还不如自己实现一套校验方案。首先校验工具必须要与组件完全解耦,其次校验工具要足够轻量,够用就好。移动端不比PC端,考虑到性能,我们一般校验表单都是在提交的时候进行校验,对不满足要求的字段进行提示(比如:字段组件样式变红,Toast轻提示等)。

思考

  • 为什么要与组件解耦?

    很多时候,我们的表单组件都并非真正意义上的表单,尤其是现在ReactVue带来的组件化时代,很多表单组件都是根据我们自己的需求来封装的,各式各样。如果每封装一个组件,就要带上完整的校验功能,那必然是痛苦的,而且有时候不同的第三方组件库很难实现校验的统一性。所以,我们校验的应该是字段,跟组件本身没有任何关系,尽管我们的字段取值是来自于组件。如此一来,我们的校验工具就适用于任何组件,不管它是否是真正意义上的表单,只要将这个组件跟校验的字段对应起来即可。所以,我们其实是对字段的校验。

  • 如何更轻量的实现对字段的校验?

    理想的方式应该是这样的:校验结果 = 校验函数(校验目标集合, 校验规则),用代码实现是这样的:result = validator(target, rules);。我们只要在提交表单的时候执行校验函数,传入待校验的字段集合和一套校验规则,拿到最后的校验结果。最后,根据结果来做一些后续的处理。

  • 目标集合、校验规则和校验结果如何定义?

    一个表单存在多个字段,目标集合应该就是一个包含所有字段的一个对象。

    1
    { name: '小明', age: 16, email: 'xiaoming@qq.com' }

    不同字段往往有不一样的校验规则,校验规则需要对每一个字段进行定义,因而也应该是一个包含所有字段的一个对象。

    1
    { name: 规则1, age: 规则2, email: 规则3 }

    规则的定义,我们一般的需求有为空判断字符串是否满足条件数字是否在指定范围之内其他特殊处理。总结起来,校验类型可以归为3类:为空、正则、自定义。为空判断最为常见,单独归为一类;正则表达式可以满足大多数的校验规则,可归为一类;前两种基本上已经满足了百分之七八十的业务场景,所以将剩下的所有校验规则通过自定义函数实现。所以最后每一个字段的校验规则是这样的:

    1
    2
    3
    4
    5
    6
    7
    {
    required: true,
    pattern: /^QQ\w{2,5}$/,
    validator() {
    // TODO...
    }
    }

    当我们拿到校验结果,我们想要知道校验结果是否通过校验结果中不通过的字段以及每个字段对应的提示语。所以,校验结果应该是一个包含所有不通过字段的对象。

    1
    const result = { name: '姓名不能为空', age: '年龄必须大于18岁' }

    对于校验结果,我们或许只关心本次校验是否通过,仅仅拿到单纯的校验结果对象不便于操作。因此,result应该还有一个hasError()函数用于返回校验结果是否出错,另外还应有一个first()函数用于返回第一个错误字段的提示信息

    1
    2
    result.hasError()   // true
    result.first() // 姓名不能为空

    根据不同的需求,可能还应该提供其他的操作方法。

实现

校验函数(validator)

思路:

  • 遍历规则集合,拿每一条规则去校验目标集合中对应的字段;
  • 首先是为空校验,空的定义应该是:nullundefined[]{}''等;
  • 若为空校验不通过,记录提示文本信息并跳过其他校验,否则继续下一个校验;
  • 然后是正则校验,同理;
  • 最后是自定义校验,自定义校验函数应该传入当前校验的值和整个目标的集合对象(可能会存在与其他字段作比较等)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function validator(target, rules) {
const ruleKeys = rules ? Object.keys(rules) : []
if (!ruleKeys.length) return new Result()
const results = ruleKeys.reduce((errors, key) => {
let value = target[key]
let tips = null
const { required, pattern, validate, alias = key, message = `请输入正确的${alias}`, trim = true } = rules[key] || {}
// 去掉字符串首位空格
trim && typeof value === 'string' && (value = value.trim())
if (typeof value === undefined || value === null || !value.length || JSON.stringify(value) === '{}') {
required && (tips = typeof required === 'string' ? required : `请输入${alias}`)
} else if (pattern && pattern instanceof RegExp && !pattern.test(value)) { // 正则校验
tips = message
} else if (typeof validate === 'function') { // 自定义校验函数
const res = validate(value, target)
tips = typeof res === 'string' ? res : (!res ? message : null)
}
return tips ? { ...errors, [key]: tips } : { ...errors }
}, {})
return new Result(results)
}

校验结果(Result)

我们看到,在validator函数中,返回了Result的实例对象。代码很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Result {
constructor(errors = {}) {
Object.assign(this, errors)
}
hasError() {
return Object.keys(this).length > 0
}
first(index = 1) {
return Object.values(this)[index - 1]
}
firstKey(index = 1) {
return Object.keys(this)[index - 1]
}
}

其实也就是在普通的对象上,扩展(原型上添加)了3个方法。

API

validator

核心校验函数:validator(target: Object, rules: Object) => result: Result

target

待校验的目标对象集合:{ name: 'Kevin', age: 18 }

rules

校验规则集合:{ name: rule1, age: rule2 }

  • alias:字段别名。比如:姓名,年龄。作为默认提示语输出,忽略则为key
  • trim:是否忽略字符串首尾空格。默认true
  • required:是否必须。为字符串时作为提示语输出。
  • pattern:正则表达式。
  • message:作为校验提示语输出,忽略则输出默认提示语。
  • validate:自定义校验函数,validate(value, target)。返回字符串时作为此次校验不通过的提示语输出,或者返回boolean表示是否通过本次校验,返回fasle时输出默认提示语。

Result

包括所有不通过校验的字段集合: { name: '姓名不能为空', age: '年龄必须大于12岁' }

方法:

  • result.hasError():本次校验是否有错(不通过)。
  • result.first([index: Number]):校验结果中第一个字段的提示语。index指定第几个字段,默认为1
  • result.firstKey([index: Number]):校验结果中第一个字段的keyindex使用同上。

应用

该校验库已经发布到npm仓库,可通过npmyarn工具进行下载。

1
$ yarn add @moohng/validator

Vue中使用

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
<template>
<x-form>
<x-input label="姓名" v-modal="form.name" />
<x-upload label="头像" v-model="form.avatars" />
<x-select label="性别" v-model="form.sex" />
<x-input-number label="年龄" v-model="form.age" />
<x-button type="submit" @click="onSubmit">提交<x-button>
</x-form>
</tempalte>

<srcipt>
import { validator } from '@moohng/validator'
import rules from './rules'

export default {
data() {
return {
result: null,
form: {}
}
},
methods: {
onSubmit() {
this.result = validator(form, rules)
if (this.result.hasError()) {
this.$toast(this.result.first())
} else {
// ...
}
}
}
}
</script>

假如你需要在校验报错之后对相应的组件进行样式上的处理,可通过响应式的result去完成:

1
2
3
4
5
6
7
8
9
<template>
<x-form>
<x-input
:class="{ 'error': result.name }"
label="姓名"
v-modal="form.name"
/>
</x-form>
</tempalte>

更优雅的做法是通过一个自定义指令去完成这些事情,比如一些滚动、聚焦、失焦、样式切换等行为。你需要记住的是:你已经拿到了这个校验结果,这个结果已包含了你需要的信息,且是响应式的(在data中已预先定义),之后的一切处理都可通过这个result对象去自行扩展。

最后

如果觉得不错,请大家多多支持~

有时候,手一抖就把一个几十M甚至上百M的文件提交到了git仓库中,这下整个人都不好了有木有。因为这个大文件直接是删不掉的,它会一直占用着你的项目空间大小,直到你实在受不了最后删库。以后每次首次拉取和推送项目的时候都是一个漫长的等待过程,我就是有过这样的经历才决定写这样的一篇文章,留着备用。

那么如何在不破坏git仓库的前提下完美删除没用的大文件

一、找到项目中的大文件

1
2
3
4
# 查看项目空间占用情况(只是查看,可以不运行)
$ git count-objects -v
# 对文件进行排序,并列出前5大文件(根据自己的需求来)
$ git verify-pack -v .git/objects/pack/pack-*.idx | sort -k 3 -g | tail -5

如果运行报错,则先运行git gc命令,再运行上面的命令。成功之后会列出文件的hash值以及大小,然后根据hash值来确定文件的名称:

1
2
# 根据文件的hash值过滤
$ git rev-list --objects --all | grep 8f10e

二、删除找到的大文件

1
$ git filter-branch --index-filter 'git rm --cached --ignore-unmatch <your-file-name>'

之后复制以下的命令一步一步执行:

1
2
3
4
5
6
7
8
9
$ rm -rf .git/refs/original/

$ git reflog expire --expire=now --all

$ git fsck --full --unreachable

$ git repack -A -d

$ git gc --aggressive --prune=now

我也不知道每一条具体有什么用,反正一步一步执行就对了。

三、强制推送到远程仓库

1
$ git push --force [remote] [branch-name]

这下整个世界终于清静了!

假如你已经知道了什么是异步,并且已经写过很多的异步代码。这篇文章主要介绍几种对异步代码的处理,即异步编码姿势:

  1. 回调函数;
  2. Promise;
  3. 迭代器、生成器;
  4. async/await。

重点在第3、4部分。

回调函数

这个没什么好说的,直接看一段代码:

1
2
3
4
5
6
7
8
9
const fs = require('fs');

fs.readFile('config.json', (err, data) => {
if (err) {
console.error(err);
} else {
console.log(data);
}
});

后面部分都以该读取文件操作为例来讲解。

Promise

Promise就是为异步而生的,主要是为了解决所谓的回调地狱问题。Promise的三个状态:pendingfulfilledrejected

通常的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require('fs');

const promise = new promise((resolve, reject) => {
fs.readFile('config.json', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});

promise.then(data => {
console.log(data);
}).catch(err => {
console.error(err);
});

需要注意一点的是:new Promise()传入的函数会立即执行,thencatch中传入的函数才是异步执行的。

then方法何时执行?取决于两点:

  1. promise何时变成完成状态(fulfilled);
  2. 在异步队列中的位置。

迭代器、生成器

概念的理解

先理解两个概念:生成器是一个返回迭代器的函数;那么迭代器就是生成器执行后返回的结果(对象)。所以,生成器是函数,迭代器是对象(很容易弄混的两个概念)。

首先,生成器是一个函数,这是一个特殊的函数,函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这就是一个生成器(函数)
function *createIterator() {
const a = yield 1;
const b = yield a + 2;
yield b + 3;
}

// 这就是一个迭代器(对象)
const iterator = createIterator();

// 注释部分是next方法执行的返回值
iterator.next(5); // {value: 1, done: false} 执行完这句并没有给a赋值
iterator.next(1); // {value: 3, done: false} 执行这句的时候才会给a赋值1,(next传进去的值)
iterator.next(5); // {value: 8, done: false} 执行这句的时候才会给b赋值5
iterator.next(); // {value: undefined, done: true}

yield 返回值取决于 next 方法传进去的值,不是 yield 后面表达式的值

异步的实现

看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('fs');

// 定义一个读取文件的函数,下面所有用到的地方均来自于此
function readFile(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}

这是node.js中一个简单的读取文件的异步操作,因为用了Promise,所以正常的使用应该是这样的:

1
2
3
4
5
readFile('config.json').then(data => {
console.log(data);
}).catch(err => {
console.error(err);
})

其实这就是上面介绍的Promise对异步的处理。假如我们有这样一个想法,希望代码是这样的:

1
2
3
4
5
6
7
try {
// 同步读取,避免回调
const data = readFile('config.json');
console.log(data);
} catch (err) {
console.error(err);
}

我们知道,正常情况下,这段代码肯定不会如期执行,因为我们的data其实是一个promise对象。但是假如有这样一个容器,它能如期的执行我们上面的这段代码,我们只需要把代码丢进这个特殊的容器里。注意到没有,上面这段代码其实是一段同步的代码,通过同步的代码实现异步的操作,这似乎是一个很完美的想法,只是首先我们需要有这样的一个容器。

运行容器

运行异步代码的容器:

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
// 运行生成器函数的一个容器
// 参数必须是一个生成器
function run(gen) {
// 创建迭代器
const task = gen();
// 开始执行
let result = task.next();

(function step() {
if (!result.done) {
// 用Promise处理
// 解释:无论result.value本身是不是promise对象,都会作为一个promise对象来异步处理
const promise = Promise.resolve(result.value);
promise.then(value => {
// 把本次执行的结果返回
// 也就是语句 const value = yield func(); 的返回值
result = task.next(value);
// 继续
step();
}).catch(err => {
result = task.throw(err);
// 继续
step();
})
}
}());
}

现在,我们有了这样的一个容器run,把读取文件的那段“同步”代码丢进这个容器里:

1
2
3
4
5
6
7
8
9
run(function *() {
try {
// 注意这里多了一个 yield
const data = yield readFile('config.json');
console.log(data);
} catch (err) {
console.error(err);
}
});

现在,我们的代码便能如期的执行了!

简单的解释一下,我们将读取文件的这段“同步”代码包装成了一个生成器函数,然后传给run函数去处理。在run函数内部首先执行这个生成器函数并返回了一个迭代器对象,当第一次执行let result = task.next()的时候,执行的就是readFile('config.json')这句,而这个函数会异步去读取文件并立马返回一个promise对象。所以result的值就是{value: promise, done: false}。由于result.value本身是一个promise对象,所以执行const promise = Promise.resolve(result.value)这句的时候返回的仍然是传入的那个promise对象(也就是result.value)。当读取文件操作完成之后,才会执行thencatch中的代码,在thenresult = task.next(value)这句代码就会让之前卡住的yield readFile('config.json')往后执行,也就是data接收到value的值,然后打印出来。

如果你对迭代器/生成器这块不熟的话,理解起来可能比较痛苦,建议先去补补这方面的知识。

其实,github上已经有人提供了run这样的容器,叫做co。所以,我们只要把注意力放在容器中的生成器里面的代码上面就可以了。

注意点

run容器中yield之后所有的代码都已经是异步执行的了,所以不管yield后面跟的是不是一个promise对象,后面的代码都是异步的。看一个简单的例子:

1
2
3
4
5
6
7
8
9
const add = (a, b) => a + b;

run(function *(){
console.log('run 开始执行');
const sum = yield add(1, 2);
console.log('sum:', sum);
});

console.log('结束了!');

这段代码中yield后面跟的是一个add函数,函数的返回值是一个数值3,并非一个promise对象或其他异步操作。但这段代码执行的结果是:

1
2
3
// run 开始执行
// 结束了!
// sum: 3

哪怕yield后面跟的不是一个函数,直接是一个数值3,执行的结果也是跟上面一样。

为什么?

注意在run中,我们是通过Promise.resolve(result.value)来处理的,result.value就是yield后面跟的东西。对Promise比较熟悉的话应该知道,Promise.resolve()传入的参数如果是一个promise对象,那么直接返回这个对象,如果传入的不是一个promise对象,那么会返回一个新创建的promise对象,并且是完成状态。也就是说Promise.resolve无论如何都会返回一个promise对象,而只有执行了then方法中的result = task.next(value)这句代码之后,yield之后的代码才会继续执行,(sum也才会接收到传过来的值)。因为result = task.next(value)是异步执行的,所以yield之后的代码自然就是异步的了。

async/await

如果你看懂了上面的介绍,那么理解async/await就很轻松了;如果你觉得上面的写法很操蛋,那么下面的写法就是一个字爽。

异步实现

先直接上代码:

1
2
3
4
5
6
7
8
9
10
11
async function run() {
try {
// 这里的 readFile 是上面定义的函数
const data = await readFile('config.json');
console.log(data);
} catch(err) {
console.error(err);
}
}

run();

就是这么简单!一眼看上去,跟上面第3部分的代码有些相像,只是yield变成了await*变成了async,外面多了一个容器run

再对比代码的执行顺序:

1
2
3
4
5
6
7
8
9
10
const add = (a, b) => a + b;

async function run(){
console.log('run 开始执行');
const sum = await add(1, 2);
console.log('sum:', sum);
}

run();
console.log('结束了!');

执行结果:

1
2
3
// run 开始执行
// 结束了!
// sum: 3

有木有很惊讶?就连执行的顺序都跟yield实现的方式一样。而且再也不用管什么容器了,看上去更加直观。这就是所谓的用同步的代码方式去写异步的操作,借用一下老外的说法:让那些烦人的回调见鬼去吧。

虽然这里不用管什么运行容器之类的东西了,但是理解它实现的原理还是很重要的。我不知道async/await是否可以理解成yield实现异步的语法糖,只不过async/await纳入ES7的标准了,而yield的写法是我们自己实现的(比如运行容器run就是我们自己封装的,你也可以根据需求扩展出更强大的功能来)。

最后

感谢阅读和分享!

首先指出,GitGithub不是一个概念,完全没有可比性。

Git可以说是每个程序员必备的技能,Github是检验一个程序员是否合格的标准。反正这东西虽然不一定要十分精通,但一定要能懂会用。Git是一个实用的版本控制(代码管理)工具,Github则是一个仓库托管(代码共享)平台,全国大神聚集的地方,装逼神器。

关于Git的详细教程建议阅读其官方文档,中文版,就是有点多,对于一般人来说阅读前三章即可。本篇文章主要介绍下面这几个方面:

  • Git的基本使用
  • Git与GitHub搭配使用
  • Git分支
  • GitHub静态站点

这是我总结的对于一般开发人员来说,比较实用的几个方面。本篇文章,只会引入几个概念和一些比较重要的思想,不是一篇纯教程。

Git的基本使用

Git是一个分布式版本控制工具,对应的SVN则是集中式版本控制工具。如果你不知道SVN,那也没必要去学它了(除非工作需要),直接学Git可能更实用一点。

分布式和集中式

简单的说,集中式,就是将项目集中在一台服务器上进行管理。而分布式就是将项目分布在各台计算机或服务器中进行管理。分布式管理中每台计算机都是一个完整的仓库,而集中式管理中只能在服务器中进行版本管理。听起来可能很模糊,详细请参考官方介绍

基本使用

  1. 配置用户信息
1
2
3
# 名称和邮箱随意, Git仅用于记录身份
$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com
  1. 基本命令
1
2
3
4
5
6
7
8
9
# 在项目根目录下运行 初始化一个本地仓库
$ git init
# 跟踪文件 或将文件添加到 暂存区
$ git add 文件名
# 提交 将暂存区的文件提交到版本控制中
$ git commit -m "提交信息"

# 查看当前版本状态 有事没事多敲敲
$ git status

这是一个最基本的操作流程。但是实际使用中,总是不会如此顺利,会遇到各种问题和需求。建议:

  • 遇到问题要多使用--help-h参数查看帮助
  • 多使用git status命令
1
2
3
4
5
# 比如查看push相关的用法
$ git push -h
# 或
$ git push --help

这里补充一下,Git有三个工作区域:工作目录、暂存区、Git仓库。我们写代码可以直接操作的是工作目录,通过git add将文件添加到暂存区git commit指令将文件提交到Git仓库

实用技巧

  1. 放弃本次工作目录中的修改
1
$ git checkout -- 文件名
  1. 将暂存区的文件移除到工作目录
1
$ git reset HEAD 文件名
  1. .gitignore不起作用
1
2
# 移除git仓库中所有文件,不会从真实的目录移除
$ git rm -r --cached .

这种情况是Git仓库中已经有了(跟踪了)某个文件,然后又修改了.gitignore文件,所以需要用上面的命令将要忽略的文件从Git仓库中移除。.代表移除所有文件。

  1. 执行命令报错Unable to create '/.git/index.lock': File exists之类的
1
2
# 删除该文件即可
$ rm -rf .git/index.lock

出现该问题一般都是之前手动或其他问题而终止了某条命令。

Git与GitHub搭配使用

对于个人来说,Git的远程仓库大多都是github。要将一个本地仓库推送到github上,首先必须github上要存在一个与本地同名的仓库,并且与本地仓库关联起来。

基本使用

你有两种方式可以选择,第一就是首先在github上建立项目仓库,然后克隆到本地使用。

1
$ git clone git@github.com:username/project-name.git

第二种就是本地已经存在一个项目仓库,然后先在github上建立一个同名的仓库,并关联起来。

1
2
3
4
# 添加远程仓库
$ git remote add origin git@github.com:username/project-name.git
# 推送到远程服务器
$ git push -u origin master

如果push失败,先pull更新到本地,然后再执行上面的push命令。这里的origin是远程仓库的名称,也是默认的。一个本地的Git仓库可以添加多个远程仓库,远程仓库名用来区分每个远程仓库(一般可能用不上,对于个人来说,远程仓库一般都是github一个)。

配置SSH

如果要使用github,配置一个SSH公钥也是必不可少的。如果你执行某项操作提示你没有权限,那么很有可能就是没有正确配置SSH公钥。

一台本地计算机对应一个SSH公钥,一个github账号可以添加多个SSH公钥,也就是说可以通过多台电脑来管理一个github账号下的项目仓库。

1
2
3
4
# 生成SSH公钥 一路回车即可
$ ssh-keygen
# 查看生成的SSH公钥
$ cat ~/.ssh/id_rsa.pub

然后,复制所有查看到的内容,添加到github上即可。

Git分支

Git的另一个重要的特性就是分支,要学好分支,必须得先弄懂Git分支的本质。

理解分支

每建立一个Git仓库,默认就有一个master分支(主分支)。**Git中的分支本质上就是一个指向commit对象的指针**。每commit一次,就对应一个提交记录(暂且就叫版本号),把这些提交记录想象成一条串联起来的方块,而分支就是指向这些方块的指针。

因此,在一个分支上的所有修改和提交只会将当前分支的指向往最新的版本移动,而其他分支依旧指向原来的提交版本,不会有任何影响。

Git中可以有很多分支,HEAD指针指向当前的分支,建议参考官方解释,图文例子很形象,一定要理解它的本质和原理。

基本操作

1
2
3
4
5
6
7
8
9
# 创建分支
$ git branch 分支名
# 创建并切换到分支
$ git checkout -b 分支名

# 删除分支
$ git branch -d 分支名
# 合并其他分支到当前分支
$ git meger 分支名

对于新手,建议不要在重要的项目上试用各种分支操作,不然你会越高越乱,最后可能就回不去了。

GitHub静态站点

github有一个十分强大的特性,就是每一个仓库,都可以是一个可访问的静态站点,也就是说,你可以将html文件放在仓库中,可以通过域名的方式来访问这个页面。

github提供一个主站点,它的仓库名必须是username.github.iousername就是你自己的用户名),默认访问的域名是username.github.io,而且还可以配置自己的域名(比如baidu.com,前提是这个域名是你的)。其他项目站点都只能通过username.github.io/project-name的形式访问。大多数人都会用这个主站点搭建一个个人的主页或博客之类的,网上与之相关的教程很多,对于非专业的前端开发人员,大多就是采用hexo来快速搭建自己的博客系统。有兴趣的可以自己百度hexo关键字,会有很多相关的教程。

其他项目有三种方法将仓库配置为可访问的站点,具体请参考这里

后记

Git对于刚接触的人可能并不友好,一路走来,我不知道弄坏了多少个项目仓库。最好是身边有会的人指导,这样会少走很多弯路。我的前端开发之路只有我一个人,而且所有东西全部都是靠自学,吃过的苦踩过的坑也是不计其数。

越是让你感觉到害怕的事情,就越要去面对它。

什么是插件

Vue的插件一般就是用来扩展Vue的功能。比如,当需要Vue实现Ajax请求功能,我们希望通过this.$get(url)的形式就可以发送一个get请求。那么,我们就需要给Vue的实例添加一个$get方法,Vue实例本身是没有这个方法的。

Vue的一些插件:

  • vuex:官方状态管理插件;
  • vue-router:官方路由插件;
  • vue-resource:Ajax请求插件;
  • vue-element:饿了么出品的组件库。

如何使用插件

在创建Vue实例之前,通过全局方法Vue.use()来使用插件:

1
2
3
4
5
// 使用 MyPlugin 插件
Vue.use(MyPlugin)

// 或 传入一个选项参数,options 一般是一个对象
Vue.use(MyPlugin, options)

是不是很简单,好像也没有什么好说的。

有时候,我们看到某些插件使用起来可能有些不一样。比如使用vuex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// store.js 文件中
import Vuex from 'vuex'
import Vue from 'vue'
import state from './state'
import mutations from './mutations'
import actions from './actions'

// 注册插件
Vue.use(Vuex)

const store = new Vuex.Store({
state,
mutations,
actions
})

export default store
1
2
3
4
5
6
7
8
9
10
// main.js 文件中
import Vue form 'vue'
import App from './App'
import store from './store'

new Vue({
el: '#app',
store,
render: h => h(App)
})

其实本质上还是一样的,也是通过Vue.use()方法注册插件。只不过它有一个store对象,然后并将store对象作为Vue根实例的属性,以便组件通过this.$store这种形式来访问。

自定义插件

其实当通过Vue.use()注册插件时,内部会自动调用插件的install()方法。也就是说,插件必须要提供一个公开的install()方法,作为接口。该方法第一个参数是Vue,第二个参数是可选的options对象。

总结起来说,插件是一个对象。该对象要有一个公开的install()方法,那么写起来可能是这样的:

1
2
3
4
5
6
const MyPlugin = {}
MyPlugin.install = function (Vue, options) {
// ...
}

export default MyPlugin

install()方法中,我们通过参数可以拿到Vue对象,那么,我们就可以对它做很多事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法和属性
Vue.myProperty = 'Hello Vue',

// 2. 添加全局的自定义指令
Vue.directive('name', function (el, binding) {
// ...
})

// 3. 混合
Vue.mixin({
created () {
// ...
}
})

// 4. 添加实例方法
// 通过原型为一个对象添加实例方法
// 在 Vue 实例中,通过 this.$get() 就可以调用该方法
Vue.prototype.$get = function () {
// ...
}
}

插件的几种写法

这里直接就看几个插件的源码吧,看看他们是怎么写的,其实我也是参照了这些源码才真正弄明白了插件是怎么一回事。源码很长,这里只说一些关键点。

vue-touch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 外面是一个立即执行函数,控制作用域
// 前面的分号应该是为了更好的兼容其他js源码吧
;(function () {
var vueTouch = {}

// 这里没有接收第二个参数
vueTouch.install = function (Vue) {
// ...
}

// 导出 vueTouch 插件
if (typeof exports == "object") {
module.exports = vueTouch
} else if (typeof define == "function" && define.amd) {
define([], function(){ return vueTouch })
} else if (window.Vue) {
// 如果 Vue 作为全局对象,则自动使用插件
// 也就是说,当我们在HTML文档中通过script直接引用 vue 和 vueTouch 时,不需要手动注册
window.VueTouch = vueTouch
Vue.use(vueTouch)
}
})()

vue-router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { install } from './install'

export default class VueRouter {
// 定义了一个静态方法, ES6 新增的语法
// 用其他语言理解也就相当于一个类方法,通过类名调用
static install: () => void
}

// 这里 install 是一个从外部引入的函数
VueRouter.install = install

// 自动注册插件
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}

vuex

1
2
3
4
5
6
7
import { Store, install } from './store'

export default {
install,
// ...
}
// 这个最简单直观,没啥好说的

vue-resource(重点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 这个。。。
// 我也不知道如何解释
// 这里好像直接用 plugin 代替了 install 方法
// 当使用 Vue.use(vueResource) 时,会调用该函数 ???
// 先暂且这么认为吧
function plugin(Vue) {
// 避免重复注册
if (plugin.installed) {
return
}

// ...
}

// 自动注册插件
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(plugin);
}

export default plugin;

Vue.use源码解读(一点要看)

针对vue-resource插件问题,我查看了一下vue的源码,它的源码是这样的:

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
Vue.use = function (plugin: Function | Object) {
/* istanbul ignore if */
// 保证同一个插件只安装一次
if (plugin.installed) {
return
}
// additional parameters
// 这句的作用应该是去掉第一个参数,然后转换成数组
const args = toArray(arguments, 1)
// 将Vue作为数组的第一个元素,以便传入插件中
args.unshift(this)
// 插件有install接口,并且是一个函数
if (typeof plugin.install === 'function') {
// 在plugin作用域下调用 install ,并传入拼接好的参数
// 在 install 中,this 指向 plugin
plugin.install.apply(plugin, args)

// 插件本身是一个函数
// 解释vue-resource写法的关键点在这
} else if (typeof plugin === 'function') {
// 在全局作用域下调用 plugin 函数
// plugin 中,this 指向 window
plugin.apply(null, args)
}
plugin.installed = true
return this
}

通过源码,我们知道,Vue插件可以是一个对象或者是一个函数。只有当插件实现了 install 接口时(有install这个函数时),才会调用插件的install方法;否则再判断插件本身是否是一个函数,如果是,就直接调用它。

现在就能很好的解释vue-resource插件的写法了。好吧,我也是刚刚得知,又长了一点见识。

其实官网也有说明

Vue.use(plugin)

作为一个Web前端开发人员,一些服务器端的知识也是必不可少的。我之前也有了解过服务器端,可是对有的东西仍是一知半解的。今天在知乎中看到一篇文章,把正向代理和反向代理讲解的很透彻。由此,我便想站在一个前端开发者的角度,谈谈正向代理和反向代理。

有哪些需求场景

先不谈概念,先说需求。

  1. 对于一个程序员来说,会科学上网是必不可少的。有的东西,在国内是访问不了的,那么如何访问像谷歌这样的网站呢?
  2. 有时候,我们写了一个前端应用(Web App),如何部署到服务器上呢?如果我们有多个应用,又如何部署呢?

这可能是大多数前端人员都会有的需求,因为我最近也是对第二个需求犯困。对于我们来说,可能最简单的方式就是直接托管到github上,我们知道,Github是可以开启静态页面功能的。注意这里的“静态”二字,也就是说,如果我们的应用需要连接数据库,需要实现后端路由(当然,也可以在前端实现路由技术),就需要我们自己部署服务器,并且写服务器端的代码。幸运的是,现在有了Node.js,我们可以直接通过JavaScript来写后端代码。这可能扯的有点远,有点超出前端的范畴了,但是,前端的终极目标是“全栈”啊。

由于JavaScript的越来越强大,原本应该在服务端做的事情,现在很多都是在前端来完成了。后面有时间会专门写一篇关于Vue的前端路由(vue-router)的文章。

正向代理和反向代理

原文看这里

正向代理

比如,客户端要访问Google,但是不直接去访问,而且让一个代理服务器去访问Google,然后代理服务器再将访问到的信息返回给客户端。这里的代理服务器所实现的功能就叫做正向代理

特点:Google并不知道是哪个客户端访问的。

根据这个特点,我们就可以在国外部署一个代理服务器,这个服务器有正向代理的功能。我们就可以通过这个代理服务器实现科学上网,盗一张图说明一下。

正向代理

反向代理

对于百度来说,服务器肯定不止一台,后面可能会有成千上万的服务器。当我们去访问百度服务器的时候,百度服务器会将我们的请求转发给背后的真实的服务器。这个过程中,百度服务器实现的功能就是反向代理

特点:客户端不知道访问的真实的服务器是哪一个。

反向代理

反向代理就是这个意思,至于如何实现反向代理,我也在摸索中,不过网上也有不少Nginx实现反向代理的教程。好吧,又得需要学习不少Nginx的知识,着实感觉自己在前端开发的大坑里越走越远、越走越偏了。

反向代理部署Web应用

Nginx是一个HTTP服务器,主要用来处理静态资源和作为反向代理,还有一个叫Apache的东西,不过现在好像用Nginx的多。

乱七八糟的概念

Nginx本质上就是一个运行在Linux服务器上的一个应用程序,它可以监听来自客户端的http请求,然后返回服务器上固定的资源(HTML文档、音视频、图片等)。

http的请求默认是80端口,所以一般Nginx只监听80端口(当然也可以监听其他端口),然后根据不同的路由参数或其他判断,反向代理到其他的服务器(真实的服务器)。比如,我们访问www.baidu.com的时候,从PC端和手机端访问的页面是不一样的。

部署Web应用

何为静态资源?就是服务器上存在的一个实实在在的文件,Nginx本身不会执行任何脚本语言,而是直接返回某个资源文件。很多时候,我们可能需要执行一些像phpPython等服务器脚本,通过这些脚本从数据库中获取数据,拼接成不同的HTML文件,最后返回给浏览器。

一个典型的例子就是Vue的服务端渲染,我们一般使用的可能都是浏览器直接渲染。但是对于一个复杂而庞大的Web应用,浏览器渲染显然不合适,这时候就需要部署一个Web服务器,可以是Node.js搭建的服务器,因为我们可以直接在服务器端使用JavaScript语言。然后,我们再通过Nginx服务器反向代理到我们的Web服务器

可能,这里有点迷惑:我们直接访问我们的Web服务器不就行了,干嘛还要通过Nginx服务器来作反向代理呢?

的确,我们也可以直接访问我们Web服务器。但是你想,一般我们可能只有一个域名,也就是说,对外只提供一个可访问的接口。用反向代理的话,我们就只需要对外提供Nginx服务器的访问链接。而我们的Web应用可能不止一个,你不可能为每一个Web应用都绑定一个域名吧。

其实就像前面说的,现在JavaScript越来越强大,很多原本在服务器端要做的事情在前端也能够完成。对于我们前端开发人员来说,只要后台提供数据接口,页面的动态渲染在前端完全可以自己实现。

正向代理实现科学上网

科学上网的工具有很多,我现在正在使用的是ShadowSocks(影梭)。至于服务器搭建教程网上真的很多,自行百度就好。

那么,首先你得有一台服务器,必须是国外或香港的服务器。这里有一些关于服务器的概念,我想有必要说一下:

  • VPS:Virtual Private Server,虚拟专用服务器。它是将一台真实的物理服务器通过一种虚拟化技术分成多个虚拟的服务器,俗称VPS。有独立的操作系统和IP地址。
  • 云服务器:跟VPS不同,它是通过多个CPU、内存、硬盘组成的,典型的就是阿里云服务器。比如我们可以动态的选择几核、多大内存、多大硬盘,并且它不受物理服务器的影响。有独立的操作系统和IP地址
  • 虚拟主机:也就是共享主机,它将一台真实的物理服务器分成多个主机,可以托管多个网站。它跟VPS最大的不同在于,没有独立的操作系统和IP地址,用户不能灵活地搭建自己想要的服务器,基本上只能用来做网站。

因此,基于以上的介绍,我们如果要搭建ShadowSocks服务器,就必须选择VPS或云服务器。我建议一般选择VPS就好了,便宜嘛,对于一个上网工具来说,足够了。如果我们要部署我们自己的服务器,建议选择云服务器,安全灵活。

后记

这篇文章写得有点乱,我现在也是正在研究这方面的东西,越到后面逻辑就越不清晰了,暂且就当作是自己的学习笔记好了。

背景介绍

一般的,我们开发一个前端项目通常是在本地通过Node.js搭一个服务器,所有的开发测试过程基本上都是在本地搞定。有时候,我们需要把我们的作品上线,好让更多的人能够访问到。比较传统的方法可能就是将本地代码通过sshftp等方式上传到服务器,然后通过ssh登入到服务器,配置好环境,然后手动启动我们的应用。

然而,很多时候我们的源码会不断的更新,如果全部由我们手动上传然后再启动,总显得太过繁琐。作为一个高大上的前端开发人员,这种方法实在太Low了。

通常,我们的代码都会用到Git版本控制工具。本篇文章,主要介绍基于 git + pm2 + node 的一键部署应用到服务器。

写在前面

网上大部分教程都只介绍方法,然而并没有提到实现原理,对于没有这方面概念的人来说,根本就不知道这是在干嘛。

三个概念

  • Git服务器:用来保存你代码的仓库,比如:Github。也可以是你自己搭建的Git服务器。为了统一,后面全部都叫Github
  • 目标服务器:就是你要将你的项目部署到的那个服务器,以下统一都叫服务器(Server)
  • 本地环境:就是你开发用的PC,就叫它Local吧。

实现原理

我不会画图,可以想象一下这个流程:我们在本地(Local)写好一个项目,再提交(commit)到远程Github上。然后让我们的目标服务器(Server)去远程的Github上获取(clone或pull)我们刚刚提交的最新版本,并通过npm install命令来安装所需的模块,最后启动服务器。

每当我们项目更新后,再重新执行此过程。这些过程都是通过pm2来实现的,而不用我们一步一步的手动去操作。

环境搭建

服务器端(Server)

需要安装node.jspm2git

以下所有命令只针对CentOS 7发行版,其他版本可供参考,其原理都是一样的。

1
2
3
4
5
6
7
8
9
10
11
# 更新 yum
$ yum update -y

# 1. 安装node.js
$ yum install nodejs -y

# 2. 安装git
$ yum install git -y

# 3. 安装pm2
$ npm install -g pm2

建议使用yum来安装,会省去很多麻烦。如果通过其他方式安装,必须要保证nodenpmgitpm2都是全局安装(已添加到当前用户的环境变量中)。

本地(Local)

与服务器(Server)端一样,需要正确安装nodegitpm2

配置SSH

根据上面的原理说明,可以得出以下3点:

  1. 本地(Local)要能够提交和拉取Github上的仓库(这个就不做介绍了,只要你使用GitHub,一般都已经配置好了);
  2. 服务器(Server)要能够拉取Github上的仓库;
  3. 本地(Local)要能够免密码登录到服务器(Server)。

这也是3个前提条件,没有配置好后面就无法正常部署。

在Github上添加Deploy Keys

在服务器(Server)上执行:

1
2
3
4
5
# 生成ssh key,一路回车即可
$ ssh-keygen -t rsa

# 查看公钥内容
$ cat ~/.ssh/id_rsa.pub

然后复制整个内容,并添加到Github上对应的项目仓库Settings下的Deploy keys中。(只读权限即可)

本地(Local)自动登录到服务器(Server)

在本地(Local)执行:

1
2
3
4
5
6
7
# 生成ssh key,一路回车即可
$ ssh-keygen -t rsa

# 将本地key添加到服务器的authorized_keys中
# name 是你的服务器用户名 server 是你服务器的地址
# 默认是22端口,否则你需要指定端口号
$ ssh-copy-id name@serer

pm2部署

如果你已经配置好环境,接下来就可以进入主题了。

这里给一个官方的连接吧,我基本上也是参照官方介绍来操作的。

配置文件介绍

在项目的根目录下,执行:

1
$ pm2 ecosystem

然后会在根目录下生成一个ecosystem.config.js文件,官方文档中写的是ecosystem.json文件,可能两种都支持吧,这不是重点,主要看里面的配置:

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
// ecosystem.config.js

module.exports = {
/**
* Application configuration section 应用配置部分
* http://pm2.keymetrics.io/docs/usage/application-declaration/
*/
apps : [

// First application
{
// 项目名称
name : 'wchat-sv',
// 程序入口
script : 'index.js',
env: {
COMMON_VARIABLE: 'true'
},
env_production : {
NODE_ENV: 'production'
}
},
// 还可以配置第二个应用
{
// ...
}
],

/**
* Deployment section 部署部分
* http://pm2.keymetrics.io/docs/usage/deployment/
*/
deploy : {
production : {
// 因为pm2要登录到服务器(Server)中执行命令,所以要提供 name 和 host
// 这里没有提供密码,也就是为什么要配置ssh免密码登录
user : 'root', // 服务器用户名
host : '45.63.48.141', // 服务器地址

// 服务器(Server)需要获取GitHub上的仓库
// 所以要配置Deploy Keys
ref : 'origin/master', // 仓库名称,没有更改过的话默认即可
repo : 'git@github.com:moohng/wchat-sv.git', // Github上的仓库地址

path : '/root/server/production', // 应用部署到服务器的路径
'post-deploy' : 'npm install && pm2 reload ecosystem.config.js --env production' // 在服务器上执行的脚本命令,会在从Github上获取到最新的版本后执行
},
dev : {
user : 'node',
host : '212.83.163.1',
ref : 'origin/master',
repo : 'git@github.com:repo.git',
path : '/var/www/development',
'post-deploy' : 'npm install && pm2 reload ecosystem.config.js --env dev',
env : {
NODE_ENV: 'dev'
}
}
}
};

该配置主要包括两个部分:appsdeploy

apps是一个数组,可以配置多个应用。其中name是应用名称(随意),script是你应用启动的入口。

deploy又分为productionstaging,其实也就是两个不同的环境。

基本需求上面的配置已经够用了,更多请参考官方文档

部署

  • 初始化服务器(Server)应用
1
$ pm2 deploy ecosystem.config.js production setup
  • 开始部署
1
$ pm2 deploy ecosystem.config.js production

如果你的环境配置正确,那么现在你的应用就已经部署成功了。

错误总结

如果部署失败,根据提示信息来操作。

  1. 无法登录到你的服务器(Server):没有正确添加本地(Local)ssh key到服务器(Server)的authorized_keys中。
  2. 服务器(Server)没有权限获取Github上的项目:Github没有正确添加Deploy Keys。
  3. 服务器(Server)找不到npmgitpm2命令:没有正确全局安装。

对于第2个问题,有个注意的地方:如果你是首次从服务器获取Github上的仓库,需要先手动在服务器(Server)上执行一遍git clone ...操作,会有一个提示,输入yes即可。我猜可能是pm2不能自动输入yes来确认吧,网上都没有提到这个问题,是我自己遇到的,不知道是不是都这样。

防火墙相关操作

如果你已经部署成功,但却不能访问,那么很可能是端口没有开放。CentOS 7 默认是firewall防火墙,如果你的是iptables,请自行百度。

开放端口

1
2
3
4
5
6
7
8
9
10
11
12
# 开放某个端口,带--permanent参数是永久开放
$ firewall-cmd --zone=public --add-port=8080/tcp --permanent

# 重启
$ firewall-cmd --reload

# 查看开启的端口
$ firewall-cmd --list-ports

# --------------
# 移除开放的端口
$ firewall-cmd --zone=public --remove-port=8080/tcp --permanent

更多firewall的使用可以通过man firewall-cmd命令来查看。

pm2开机自启

1
$ pm2 startup centos

网上的教程好像还说要执行其他什么命令,不过我好像就执行这一些命令就搞定了。不知道是不是版本更新了,我使用的是目前最新的版本。

总结

我觉得这个技能很实用,也是因为最近的一个需求才了解到的,就拿出来分享一下。

差不多花了整整两个星期,终于把这个聊天APP的后台架构搭建出来了。虽然花的时间比较多,但这也是我第一次写后台,其实也并没有想象中的那么难,但也还是很折腾,尤其是在数据库这一块,几乎全部都是英文文档(看得都只想**)。

项目概述

该聊天App高仿iOS端的微信,当然没这么复杂,目前已实现功能有:

  • 用户注册、登录、注销功能;
  • 自动缓存已登录用户,关闭浏览器窗口失效;
  • 聊天室:所以在线用户之间聊天;
  • 与在线用户之间聊天;
  • 获取所有在线用户;
  • 获取好友列表;
  • 添加好友:后台接口已完成,前端目前尚未实现。

现在几乎每天都在更新,争取把它做得更像一个正规的聊天应用。不过由于该应用是基于Web页面的,用户体验和数据持久化等诸多方面肯定没法跟客户端应用相比。

前端Web界面

前端界面在一个多月前就已经差不多写出来了,苦于一直没有后台接口(API)的支持,所以仅仅只是一个界面展示,并无实际聊天的功能。

对前端我就不做深入的介绍了,主要是基于Vue来实现的。而且对于一个前端开发者来说,后台实现可能更具有挑战性。

后台实现

为了实现真实的聊天功能,我决定自己来搭建后台,这也是我第一次写后台。整个后台应用基于Node.js平台,采用express模块来搭建HTTP服务器,聊天功能采用WebSocket实现,数据库使用的是MongoDB

主要使用的技术栈包括:Node.jsexpressexpress-sessionexpress-wsmongodbmongoose

后台逻辑分析

初次写后台,最难的可能就是架构了,因为你要对整个应用的需求、实现的功能、数据的模型等有一个清晰的思路逻辑。我可能也就是在这方面花的时间是最多的,总是不知道该如何下手。很多次都是写着写着就写不下去了,因为逻辑行不通了。

遇到的问题和难点

  • 如何判断用户是否是登录状态?如何记住用户的登录状态?
  • 如何断定当前登录的用户是否成功连接了WebSocket服务器?
  • 当一个来自客户端的websocket请求时,如何判断该用户是否已登录?需要一个身份识别功能,否则谁都可以任意接入websockt服务器了。
  • HTTP服务器与WebSocket服务器之间如何并存?又如何交互?因为只有聊天功能和消息推送功能使用ws,其他所有的请求都是与http服务器通信。
  • ws服务器如何判断消息的转发目标?如果目标用户不在线又如何处理?
  • 如何搭建数据库?对于初次接触的人来说这也是个难题。
  • 如何连接和操作数据库?起码要基本的增删改查。
  • 密码加密问题,这同样是一个很大的难题。

其实问题还有很多很多,这可能对于后台开发人员来说都显得小儿科,但这些真是我开发过程中遇到的问题,当然还不止如此。到目前为止,有的问题已经解决了,有的问题仍未解决,或没有找到更好的解决方案。

其实,学习也就是一个发现问题,然后解决问题的过程。当你把一个一个的问题都解决之后,你也就在不知不觉中慢慢成长起来了。贵在坚持,也难在坚持。

模块介绍

对于上面的问题,我也是自己网上找资料,目前主要引用到了这些模块框架:

  • express:基本上是整个后台应用的支撑,HTTP和ws都是建立在此基础之上。一个Node.js上很强大的东西,可以让你快速创建一个Web应用。
  • express-session:这个是express的插件,主要用来解决上面说到的判断用户是否登录的问题。
  • express-ws:这也是一个express的插件,用来构件一个ws服务器。之前采用的是ws框架,但与express交互性太差,不好在wshttp之间通信。
  • body-parser:一个express框架,主要用来解析POST请求发过来的数据。
  • mongoose:一个用来操作mongodb数据库的框架。还有一个叫做mongolass的框架,比这个量级要轻。

主目录结构

由于是第一次写后台,后台结构分的并不是很清晰。

  • index.js:入口文件,创建一个http服务器和一个ws服务器,并连接到数据库。
  • model:该目录主要写一些与数据库交互的代码。
  • routes:这个目录主要处理路由,大部分的操作都是在该目录下进行的。

主要的架构就是这样,基本操作都在routes目录下,因为后台也就是为前端写接口。在routes目录下又分了不同的子路由,比如:frienduserwsmessage等,分别处理不同的请求。

看起来很简单,但做起来真的不容易,最可怕的是代码量大了,你会陷入一个大量重复代码和无限回调的噩梦,我想大部分人都经历过js的回调噩梦。目前也只是有了个初步的逻辑架构,后面可能会根据需求的不同而变更。代码也需要优化,有的自己一遍一遍写起来就恶心。

两个容易误会的概念

本篇文章主要作个整体的介绍,因为该Web应用目前仍在开发中,很多功能还不确定,等后面整个逻辑清晰了再作总结。下面说两个很经典的问题,也是前端很容易误会的问题,至少我是误会了很久。

跨域

在这之前,对跨域访问是一知半解,不知道到底该如何解决这个问题。这里要提出,跨域访问不是前端的问题,其实大部分都是后台的问题。对于跨域,网上有两种解决方案:JSONP和Ajax。对于JSONP没什么研究,不作介绍,好像也并不是很实用,这里主要介绍Ajax跨域的问题。

下面是我后台解决跨域问题的方案:

1
2
3
4
5
6
7
8
9
10
app.use((req, res, next) => {
res.set({
// 跨域cookie 不能为通配符 *
'Access-Control-Allow-Origin': 'http://localhost:8808',
'Access-Control-Allow-Methods': 'GET,POST',
// 跨域cookie必须为true
'Access-Control-Allow-Credentials': true
});
next();
});

简单的说一下,跨域其实浏览器是可以正常的收到来自于服务的响应,只是无法正确的解析。通过在服务器端对响应头写入'Access-Control-Allow-Origin': '*''Access-Control-Allow-Methods': 'GET,POST',浏览器才能正确的解析服务器的响应。记住是在服务器端对响应头的操作,我之前一直误会是在前端的请求头中写入,现在想想有点傻逼了。

对于'Access-Control-Allow-Credentials': true,是用来处理跨域中cookie的问题。因为默认情况下,cookie是不允许在跨域访问中传输的。要解决这个问题,Access-Control-Allow-Origin的值就不能为通配符*,并且前端通过Ajax发起请求时也要做处理。

1
2
3
4
5
6
7
$.ajax(url, {
method: 'GET',
xhrFields: {
withCredentials: true
},
...
})

之前对Cookie的认识一直就是一种类似于缓存的东西,但具体是做什么,怎么用,并不清楚。这是要指出两点:

  • Cookie基本上都是由后台来管理的,前端不需要任何操作
  • Cookie信息会在每次发起的请求中自动携带

那么,这下就清晰多了。如果你仅仅只是搞前端,基本上是用不到Cookie的。虽然也可以通过js代码读取到cookie数据,但大部分服务器都是禁用掉此操作,也就是让你在前端无法通过js代码读取到cookie的内容,读取到的是空字符串。

因为cookie是每次发起请求都会自动携带的,所以服务器就可以通过cookie来识别用户的身份、是否处于登录状态等,就像你进入某个网站有时候会自动识别你的身份并登录。而cookie也是可以设置过期时间的,所以服务器端就可以控制你的身份多久失效,失效之后你就要重新登录了。

你可以自己尝试在浏览器的控制台通过document.cookie来获取一下网站的cookie信息。也可以尝试清除浏览器的cookie,然后再刷新你登录的网站,看是否需要重新登录。

我这个项目中用到的express-session就是通过cookie来识别用户身份的。使用express-session的好处就是你不需要自己要操作cookie,使用起来简单。

后记

我一般写文章都是针对自己实际遇到的问题来的,我目前也是在不断的学习中,过几天就会写一篇文章作个总结。