前端汪如何打造一个全功能全免费的动态站点

664 阅读18分钟
原文链接: www.awesomes.cn

话说,这是属于前端的时代......

在前面的文章中,我已经讲过了如何利用 Github 搭建一个免费的静态站点(深入 Github 主页)没错,这里是静态站点,说白了就是一个托管静态资源的存储而已,不提供运行后台代码的功能。

那么我们怎么做到静态站点展示动态内容呢?只有一种方式,那就是直接调用 JS 数据接口。例如之前的文章 探索那个所谓的 Github 简历项目 正是这样实现的,他是通过调用 Github 开放的 API 来获取每个会员的数据。

一个完整的网站在数据层面来讲就是对数据的增删改查而已,我们提供的接口需要具备这样的功能。那么我们可以用自己熟悉的后台语言撘一个 API ,然后让静态站点来调用,但是这个 API 是需要服务器来部署的,服务器是需要 money 的,而且如果你不懂后端开发怎么办?这和我今天讲的内容相冲突,记住,我们的目标是不花一分钱。

那我们再想想,既然自己搭建API行不通,那唯一的办法就是调用别人的 API了。大家熟知的一些大公司都会开放一些API,但那是别人给你开放的用于获取他们数据的接口,给你什么数据是他们决定的,而我们需要的是对自己的数据进行操作,我想要什么就得给我什么,很明显这不符合我们的要求。

我们需要的到时是什么呢?我希望是这样的:它给我们提供的接口,可以让我们自己通过JS去发送请求,附带的参数可以告知接口,我要创建一个表,我要删除一条数据,我要查找数据列表。

awesomes-cn

对的,就是它必须同时给我提供数据存储和开放接口

Parse

它是干嘛的呢?Parse 主要是为移动开发提供的一套数据操作的接口(说白了就是整个后端程序都由他来做)开发者不再需要关注后台数据操作了。同时它也提供了JS的SDK,所以我们完全可以用它来开发一个 web 应用。

awesomes-cn

那它是免费的吗?不是,不过它提供了免费的用量

awesomes-cn

可以看到,Parse 为我们提供了每秒30个请求的免费额度,对于一个小网站完全是够用了。

工作原理

用户可以从多个终端(web、IOS、Andriod)发送请求到 https://api.parse.com/1/classes/Mem,然后携带上下面格式的URL参数:

where={"name":"awesome"}

可以看到这里是要查询 nameawesomeMem 记录(下面有更多的查询条件介绍)。

Parse 服务端接受到这个请求后,会去解析里面的每个参数,从而确定要做什么操作。说白了就是在前端去写 sql 语句,以前这些都是由后端开发人员做的,现在完全开放给前端去做了。

我们不再需要为每一个数据对象的操作去写一个专门的接口,而是直接走一个 万能接口 具体做什么操作,根据传入的参数即可分析出来。

功能

Parse 提供了一个应用所需的几乎所有后台功能:

  • 数据操作
  • 用户、会话、角色
  • 文件存储
  • 坐标导航
  • 错误处理
  • 安全
  • 消息推送
  • 性能分析

所以拿它做一个完整的应用是没什么问题的,下面我们就看看查询的 JS SDK 用法。

JS SDK 概览

这里我主要讲一些大家关心的问题。

对象

Parse 上的数据以JSON键值对储存在 Parse.Object 中。用Parse.Object.extend 创建子类:

初始化创建对象,包含3个内置字段(objectId, createdAt, updatedAt

var Mem = Parse.Object.extend("Mem");
var mem = new Mem();

设置值:

mem.set("name", "hxh");

获取值

var name = mem.get("name");

保存 / 更新

mem.save({
    age: 18
},{
  success:  function(mem){...},
  error: function(mem, error) {...}
})

删除对象

mem.destroy({
  success:  function(mem){...},
  error: function(mem, error) {...}
})

删除字段值

mem.unset("age");

查询对象

var query = new Parse.Query(mem);
query.get(,{
   success:  function(mem){...},
  error: function(mem, error) {...}
})

刷新对象

mem.fetch({
    success:  function(mem){...},
  error: function(mem, error) {...}
})

对象关联

比如一个 myComment(评论实例)归属于一个 myPost(文章实例)

myComment.set("parent", myPost);

获取关联的对象

var post = myComment.get("parent");
post.fetch({
...
});

查询

1、新建查询对象

var Mem = Parse.Object.extend("Mem");
var query = new Parse.Query(mem);

2、查询条件

query.equalTo("name", "hxh");

3、执行查询

query.find({
    success: function(results) {...},
    error: function(error) {...}
})

或只返回第一个结果

query.first({
    success: function(results) {...},
    error: function(error) {...}
})

返回数量

query.count({
    success: function(count) {...},
    error: function(error) {...}
})
更多查询条件
query.notEqualTo("name", "awesomes");  //  不等于
query.greaterThan("age", 18); // 大于
query.lessThan("age", 50); //  小于
query.lessThanOrEqualTo("age", 50); //   大于等于
query.greaterThanOrEqualTo("age", 50); // 小于等于

query.containedIn("name",["hxh","awesome"]) //- 包含
query.notContainedIn("name",["hxh","awesome"]) //- 不包含


query.exists("age"); //- 存在
query.doesNotExist("age"); //- 不存在

// 字段类型为数组
query.containsAll("arrayKey", [2, 3, 4]); //- 包含数组值

// 字段类型为字符串
query.startsWith("name", "awe"); //- 以什么字符串开头

指定返回部分字段值(包括内置字段 objectId, createdAt, updatedAt

分页

query.select("name", "age");
query.skip(5); //- 忽略前5个结果
query.limit(10); // 最多返回10个结果

排序

query.ascending("age");  //- 升序
query.descending("name");  //- 降序

关联查询(一个用户有一个信息表)

query.include("info");
query.find({
    success: function(mems) {
      var info = mems[0].get("info");
    }
})

子查询(or

...
var mainQuery = Parse.Query.or(query1, query2);
...

文件资源

对于网站要用到的资源文件,我前面的文章说过,可以存放在云存储里面。但是上传需要接口,如果该云存储提供了上传的JS接口,那可以使用。

Parse 自身也为我们提供了云存储的功能,而且提供了上传文件的SDK,比较方便。如果你非得要把文件上传到一个未提供JS接口的第三方云存储里面,那可以通过 Cloud Code 后台代码去实现(见下面)

安全

说实话,在了解了 Parse JS SDK 工作方式后,我第一个关心的问题就是如何保证安全性。

大家可以想想,和 IOS 、Andriod 不一样,我们是将 APPLICATION_ID 和 REST_API_KEY 都直接暴露在页面中的,任何人都可以拿到。

因此 Parse 采用下面的方式来提升安全性:

  • 所有请求必须走 HTTPS 和 SSL,Parse 会拒绝响应非 HTTPS 的请求。
  • 对于每个数据表我们都可以设置其读写和修改权限,然后根据用户的角色和来控制权限。
  • 基于Cloud Code 的数据完整性校验,也就是你的数据格式必须满足要求才允许被操作(如在 beforeSave 做一些校验工作)
  • 基于Cloud Code 的业务逻辑

咱们这里举个例子,比如这是一个博客程序,下面有每个用户的评论,而每个用户只能删除自己的评论,别人是不能删除的。

那么咱们就可以先将评论表设置成公开可读,以及对特定的角色(或用户)开放修改权限(这个是可以在 Parse 后台做配置的)。然后通过登录记录每个用户的会话,获取到当前用户角色,后台就会做相应的判断来决定你是否有权限删除该评论。

Cloud Code

对于一些简单的应用,使用上面的客户端SDK去调用服务端接口完全是没问题的,但是一些复杂的需求就满足不了了,这个时候我们就需要真正去后台写代码解决。

Cloud Code 就是干这事的,意思就是我们在 Parse 的云端去写代码(Node)来处理复杂逻辑。它可以接收请求并控制返回内容。还可以创建定时任务、日历记录和 Webhook,简单说,就是你基本上可以完成你在普通服务端开发中做的所有事情。

结合 Cloud Code 和 Express 框架可以和普通动态网站完全一样。

一句话,Cloud Code 就是作为 Parse 原有数据操作的一个补充,增强了服务的可定制性。

托管

Parse 同样为我们提供了托管,也就是说,你可以不用 Github,直接全部使用 Parse。

和 Github 不同的是,Parse 提供的托管是可以运行 Node 后端代码的,所以你完全可以写一个 Express 应用托管到 Parse 上。

但是对于一个轻量级的 web 应用我还是偏向于将代码托管在 Github 上进行开源和版本控制。在一个分工明确的社会,得将不同的任务交给最专业的去完成。


Node 给前端开发带来了很大的改变,促进了前端开发的自动化,我们可以简化开发工作,然后利用各种工具包生成生产环境。如运行 sass src/sass/main.scss dist/css/main.css 即可编译 Sass 文件。在实际的开发过程中,我们可能会有自己的特定需求,那么我们得学会如何创建一个Node命令行工具。

命令行接口:Cmmand Line Interface,简称 CLI,是 Node 提供的一个用于命令行交互的工具,本质是基于 Node 引擎运行的。

在前面的文章 前端扫盲-之打造一个自动化的前端项目 中,给大家留了一个问题,就是如何通过执行一条命令就生成我们需要的项目结构。今天我就带着大家一步一步解答这个问题。

我们的初步设想是,在指定目录下执行一个命令(假设为autogo

autogo demo

就会生成一个目录名为 demo 的项目,里面包含有我们所需的基础文件结构。

开始

1、首先咱们创建一个程序包清单(package.json 文件)包含了该命令包的相关信息:

npm init

然后根据提示输入对应的信息,也可以一路回车,待生成好 package.json 文件后再作修改。

2、创建一个用于运行命令的脚本 bin/autogo.js

#! /usr/bin/env node
console.log("hello")

然后我们执行

node bin/autogo.js

能够看到输出了 hello,当然这不是我们想要的结果,我们是要直接运行 autogo命令就能输出 hello

3、告诉 npm 你的命令脚本文件是哪一个,这里我们需要给 package.json 添加一个 bin 字段:

{
...
 "bin": {
    "autogo": "./bin/autogo.js"
  }
  ...
  }

这里我们指定 autogo 命令的执行文件为 ./bin/autogo.js

4、启用命令行:

npm link

这里我们通过 npm link 在本地安装了这个包用于测试,然后就可以通过

autogo

来运行命令了。

可能遇到的问题

1、有的同学可能在执行 autogo 命令后会报下面的错误:

-bash: /usr/local/bin/autogo: /usr/local/bin/node^M: bad interpreter: No such file or directory

之所以出现这个错误是因为 bin/autogo.js 文件是在 windows 下做的编辑,windows 下默认的换行是 \n\r ,而 linux 下默认的换行是 \n,所以文件后的 \r 在 linux 下是不会别识别的,显示成了 ^M

要解决这个问题的办法就是改变文件的编码,这里我们需要用到 dos2unix 这个包。

首先安装

sudo apt-get install dos2unix
sudo dos2unix bin/autogo.js

问题就解决了。

2、还有的同学可能会遇到下面这个报错

: No such file or directory

报这个错是因为 #! /usr/bin/env node 没能识别出你的 node 的路径,需要将你的 node 安装路径(如/usr/local/bin/)加入到系统的 PATH 中。

其实你可以在测试环境中将这个标识换成 #!/usr/local/bin/node(这里得 which node 找到你自己的 node 路径),再运行就没问题了。但是我们之所以用 #! /usr/bin/env node 是因为这可以动态检测出不同用户各自的 node 路径,而不是写死的,毕竟不是所有用户的 node 命令都是在 /usr/local/bin/ 下。 得确保在发布之前将其改回成 #! /usr/bin/env node

到此,一个本地的 npm 命令行工具就已经成功完成了,(可参见 官方文档)接下来我们就来完善具体的功能。

创建项目结构

咱们需要的项目结构大致如下,包含了所需的文件和文件夹(详见)。

awesomes-cn

要创建上面的结构,我们可以通过程序来创建么个文件和文件夹,但是对于这么多文件,而且每个文件里或许还有更多内容,所以我们应该用一个更简便的方法。

实际上我们可以先创建一个完整的结构,然后在执行命令时,通过程序把这些文件和文件夹整个复制到目标项目文件夹中去,最后再对某些文件做一些修改即可。

按照这个思路,我们根据上面的结构,将这些文件和文件夹创建到 structure 下,然后咱们创建一个生成结构的方法 lib/generateStructure.js(这里咱们将功能模块放在了 lib/目录下)

var Promise = require("bluebird"),
    fs = Promise.promisifyAll(require('fs-extra'));


function generateStructure(project){
  return fs.copyAsync('structure', project,{clobber: true})
    .then(function(err){
      if (err) return console.error(err)
    })
}


module.exports = generateStructure;

上面的代码就是通过 fs-extra 这个包(查看文档) 将 structure 目录下的内容复制到了 project 参数的目标文件夹中。 fs-extra 是对 fs 包的一个扩展,方便我们对文件的 操作。

这里咱们用到了 bluebird查看文档),这是一个实现 Promise 的库,因为这里牵涉到了对文件的操作,所以会有异步方法,而 Promise 就是专门解决这些异步操作嵌套回调的,能将其扁平化。

自然,我们应该安装这两个包:

npm install bluebird --save
npm install fs-extra --save

这里加上 --save 参数是为了在安装后就自动将该依赖加入到 package.json 中。然后咱们改造一下 bin/autogo.js

#!/usr/bin/env node
var gs = require('../lib/generateStructure');

gs("demo");

然后执行

autogo

可以看到当前目录下生成了一个 demo 文件夹,里面包含了和 structure 相同的文件结构。

我们的目标已经初步达成了,接下来我们就来细化该命令。

命令参数

上面的命令中,我们执行 autogo 时,是生成了一个固定的 demo 项目,实际上这个名字是不能写死的,而是应该通过命令中的参数传进去。像下面这样:

autogo demo

因此,我们得在 bin/autogo.js 中去接收参数了。为了方便起见,我们这里直接使用一个专门用于处理命令行工具的包 commander文档)。

同样,首先安装

$ npm install commander --save

然后改造 bin/autogo.js 为:

#!/usr/bin/env node

var program = require('commander'),
    gs = require('../lib/generateStructure');

program
  .version(require('../package.json').version)
  .usage('[options] [project name]')
  .parse(process.argv);

var pname = program.args[0]

gs(pname);

这里的 .version() 意思是返回该命令包的版本号,即运行

autogo --version //- 返回1.0.0

会返回 package.json 中定义的版本号。

.usage() 显示基本使用方法 执行

autogo --help

会输出:

  Usage: autogo [options] [project name]

  Options:

    -h, --help     output usage information
    -V, --version  output the version number

可以看到 Commander 帮我们做好了用法(Usage) 信息,以及两个参数(Options)-h, --help-V, --version

.parse(process.argv); 是将接收到的参数加入 Commander 的处理管道。

program.args 是获取到命令后的参数,注意这里是一个数组

autogo    //- 返回  []
autogo demo  //-返回 ['demo']
autogo demo hello  //-返回 ['demo','hello']

这里咱们取第一个参数作为项目名,然后调用

var pname = program.args[0]
gs(pname);

现在我们执行:

autogo demo2

就可以看到新的项目demo2生成了,看上去我们已经完成工作了,只要运行 autogo <项目名> 就可以生成一个新的项目结构,里面包含了处理 Sass、coffee、jade 的 gulp 构建工具。

如果我们直接运行 autogo 是会报错的,因为没有传入项目名,实际上我们在运行一个命令而不传入任何参数时,可以直接返回帮助信息:

...
var pname = program.args[0]
if (!pname) program.help();
...

上面我们判断是否存在参数,如果不存在就调用 program.help() 方法,这是 commander 为我们提供的显示帮助信息的方法,可以直接调用。

那有的同学要说了,我不想用 jade ,就喜欢写原生的 HTML,很明显我们做了多余的事,而且整个结构就不那么合理了,我们需要的是一个干净的项目结构。

这个时候我们就需要把与jade 相关的文件都删掉(这里不是删 structure 目录下的文件,而是新项目下的指定文件)。与 jade 有关的文件有:

  • /structure/views/ 下的 index.jadelayouts/layout.jade
  • /structure/gulpfile.js 中的 templates 任务代码

因此,咱们得把上面这些文件和代码干掉。

移除指定模块

首先,咱们创建一个 lib/jadeWithout.js 用来移除 jade:

var Promise = require("bluebird"),
  fs = Promise.promisifyAll(require('fs-extra')),
  del = require('../lib/delFile');

var files =  ['/views/layouts/layout.jade','/views/index.jade'];
function jadeWithout(project){
  return Promise.all([del(project,files)])
    .then(function(){      
      return  console.log('remove jade success');
    })
}
module.exports = jadeWithout;

这里咱们将指定的 files 数组中的文件都删除了,这里我用了一个公共的删除文件模块 /lib/delFile.js

var Promise = require("bluebird"),
  fs = Promise.promisifyAll(require('fs-extra'));

function del(project,files){
  return files.map(function(item){
    return fs.removeAsync(project + item)
  }) 
} 
function delFile(project,files){
  return Promise.all([del(project,files)])  
}
module.exports = delFile;

因为我们这里不光有 jade,还有sasscoffee可以被移除,所以我们创建一个公共入口 withoutFile.js

var Promise = require("bluebird");

function deal(project,outs){
  return outs.map(function(item){
    var action = require('../lib/'+item+'Without');
    return action(project)
  }) 
}

function withoutFile(project,outs){
  return Promise.all([deal(project,outs)])
}
module.exports = withoutFile;

这里我们需要传入一个要移除的列表(如['sass','jade']),然后对每个模块进行删除。

最后,我们将withoutFile 引入到bin/autogo.js 中:

...
var gs = require('../lib/generateStructure'),
    wf = require('../lib/withoutFile');
...  
Promise.all([gs(pname)])
  .then(function(){
    return wf(pname,["jade",'sass'])
  })

然后我们再次执行

autogo demo

可看到控制台依次输出了

generate project success
remove jade success
remove sass success

而且目标项目中相关文件已经被删除了。

这里咱们是 wf(pname,["jade",'sass']) 写死了 outs 参数作为测试,实际上是要再传入一个数组,那么这个数组从哪儿来呢?很明显,得从命令行参数中获取。

我们希望的是这样:

autogo --without jade demo

option

commander 为我们提供了一个 option 管道来配置命令参数,修改 bin/autogo.js

...
program
  .version(require('../package.json').version)
  .usage('[options] [project name]')
  .option('-W, --without ', 'generate project without some models(value can be `sass`、`coffee`、`jade`)')
  .parse(process.argv);
...

这里咱们添加了 option,其格式为 .option('-<大写标识>, --<小写全称> <可取参数类型>', '数功能描述')

接着处理 without 参数:

...
var outs = program.without ? [program.without] : []

Promise.all([gs(pname)])
  .then(function(){
    return wf(pname,outs)
  })
...

然后咱们再运行

autogo --without jade demo

可以看到这里只移除了 jade 模块,那如果我想移除多个呢?是不是可以这样:

autogo --without [jade,sass] demo

注意,这样是会报错的,因为获取到的 program.without 是一个字符串 '[jade,sass]'而不是数组,所以咱们可以这样:

autogo --without jade,sass demo

program.without 则为'jade,sass' 然后再

 program.without.split(',')

既可以获取到一个数组了,因此咱们的代码就变成了:

 ...
var outs = program.without ? program.without.split(',') : []

Promise.all([gs(pname)])
  .then(function(){
    return wf(pname,outs)
  })

 ...

这下我们就可以这样来运行了:

 autogo demo --without sass,jade

发布

到目前为止,我们开发的 autogo 还是在本地的,现在就该将其发布到 npm 上了。

1、首先咱们得 注册一个账号

2、回到项目中,执行

npm login

输入用户名、密码和邮箱便可将本地机器与 npm 连接起来了。

3、执行

npm publish

然后回到你的 npm 个人主页,就可以看到我们发布成功了 www.npmjs.com/package/aut…

从包的路径规则来看,是没有包含用户名的,由此可知,同名的包是不会被允许的,所以大家在跟着做的时候要给项目取一个不同的名字。

然后咱们来测试一下刚刚发布的包

首先删除本地开发做的 autogo 链接

sudo npm unlink

然后

npm install autogo -g

注意这里需要带上 -g 参数,因为命令行是应该安装在全局环境中。安装成功后,我们切换到另外一个目录下,执行:

autogo demo

然而结果并非我们想象的那样:

Unhandled rejection Error: ENOENT, lstat 'structure'
    at Error (native)

意思是找不到 structure,这是怎么回事呢?

实际上当我们执行 npm install autogo -g 的时候,实际上是将命令包安装在了 /usr/local/lib/node_modules/autogo 下面,所以在执行命令的目录下是找不到 structure 文件夹的。

那该怎么办呢?我们能想到的就是,得在程序中去获取这个包安装的实际路径。

幸运的是 Node 给我们提供了 __dirname 这个变量用于获取当前执行文件的路径。 我们在 lib/generateStructure.jsconsole.log(__dirname) 会输出 /usr/local/lib/node_modules/autogo/lib ,然后我们把后面的lib 去掉就是根目录了:

var root = __dirname.replace(/autogo\/lib/,'autogo/')

function generateStructure(project,outs){
  return fs.copyAsync(root + 'structure', project)
    .then(function(err){
      return err ?  console.error(err) : console.log('generate project success');
    })
}
...

修改后,我们按照下面的方式更新,重新安装,然后

autogo demo
cd demo
npm install
gulp watch

OK 一个新的项目诞生了,准备开发吧...

更新

首先修改 package.json 配置文件中的 version 字段,比如这里我从 0.1.0 改成 0.1.1(只能大于当前版本),然后再次

npm publish

即可成功发布新版本。

想将该项目从 npm 中移除吗?执行 :

npm unpublish autogo --force

附:项目源码 github.com/awesomes-cn…