原生开发移动web单页面(step by step)5——nod
由于后面的开发要基于服务器提供的条件,因此这里很有必要从头到尾搭建一个本地服务器并且模拟提供相关的服务。首先去nodejs的官网下载安装包安装,然后创建一个文件夹,如下面的目录新建一个目录架构,如下图
架构目录图
lib文件夹主要放置后端自己写的js,主要提供后端服务
public文件夹主要放置前端的文件,也就是html,css和js代码
index.js是服务器的入口主程序, 可以处理前端的请求
调出命令行控制台将位置设置到该文件夹目录, 然后安装mime模块,输入命令如下
npm install mime
生成如下的目录架构,如图
生成架构目录图
基础环境已完成,现在开始写服务代码,打开index.js,写入下列代码
var http = require("http"),
url = require("url"),
root = __dirname,
serverFile = require("./lib/serverFile.js");
var server = http.createServer(function (req, res) {
var urlObj = url.parse(req.url, true);
var path = urlObj.pathname.toLowerCase();
if (req.method === "GET") {
serverFile.getFile(req, res, path, root);
}
}).listen(3000);
console.log("Server started on localhost: 3000");
然后再lib文件夹中创建serverFile.js, 编写下列代码
var mime = require("mime"),
fs = require("fs");
function serveFile(req, res, pathname, root) {
fs.stat(pathname, function (err, stat) {
if (err) {
if ("ENOENT" == err.code) {
res.statusCode = 404;
res.end("404");
}
else {
res.statusCode = 500;
res.end("Internal Server Error");
}
}
else {
res.setHeader("Content-Length", stat.size);
res.writeHead(200, { "Content-Type": mime.getType(pathname) });
var stream = fs.createReadStream(pathname);
stream.pipe(res);
stream.on("error", function (err) {
res.statusCode = 500;
res.end("Internal Server error");
});
}
});
}
exports.getFile = function (req, res, key, root) {
if (key.indexOf("/public/") === 0) {
serveFile(req, res, root + key, root);
}
else if (key === "/serve") {
serveFile(req, res, root + "/public/serve/index.html", root);
}
else {
res.statusCode = 404;
res.end("404");
}
}
在public文件夹中创建一个serve文件夹,把上一篇的html,css和js文件夹复制进去。接着在命令行控制台中输入
node index.js
接着在控制台上提示
Server started on localhost:3000
说明本地服务搭建成功,然后在浏览器中输入localhost:3000/serve
或者在同一局域网上的手机端浏览器输入本地ip加端口号加文件名路径,比如本机ip为192.168.1.13,那么手机浏览器上输入 http://192.168.1.13:3000/serve
现在调整前端的目录结构,如下图
前端目录
将index.html放在serve根目录下,然后其中html放置html模板,将index.html上的html模板全部迁出到在html文件夹中, js文件夹放置js文件,css放置css文件,修改index.html的代码
index.html代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>首页</title>
<link rel="stylesheet" href="/public/serve/css/index.css">
<script src="/public/serve/js/page.js"></script>
<script src="/public/serve/js/app.js"></script>
</head>
<body>
<div class="pageContainer"></div>
<script src="/public/serve/js/entry.js"></script>
<script src="/public/serve/js/register.js"></script>
<script src="/public/serve/js/goal.js"></script>
<script src="/public/serve/js/index.js"></script>
</body>
</html>
这里使用绝对路径引用,基于服务器的入口index.js的位置为准。
page.js和app.js放在head元素里面,将之前的首页命名为entry, 这里每个js代表一个Page页面, index.js代表定义初始化程序。
引入服务器后,每个页面模板都是以异步的形式加载,因此Page的渲染过程需要改变,之前都直接加载在一个页面上,直接同步读取,现在异步读取,读取完后加载到页面上,才开始绑定事件。这个过程需要放在读取完毕后的回调函数中执行,因此要在Page的对象上加入新的一个方法,代码如下
render: function (fn) {
console.error("must be override");
},
修改_initialize方法,代码如下
_initialize: function (container) {
this.getDomObj(container);
this._addEventListeners();
},
这个需要开发者自己手动调用设置模板html字符串,然后调用回调函数。
修改Page对象的构造函数, 代码如下
function Page() {
this.domList = {};
this.eventList = [];
}
接着修改App对象, 修改render的步骤,代码如下
render: function (page) {
if (this.currentPage == page) return false;
if (this.currentPage) this.currentPage._dispose();
this.currentPage = page;
var pageContainer = this.pageContainer;
page.render(function (html) {
pageContainer.innerHTML = html;
page._initialize(pageContainer);
});
}
在修改Page的实例对象之前, 首先先加入一个原型方法,它的目标是提供静态文件,如果静态文件已存在就直接调用,不存在则通过ajax去调取,然后缓存起来。以免多次请求,代码如下
fetch: function (url, bk) {
var self = Page.prototype;
var that = this;
if (self.fetch[url]) {
bk.call(this, self.fetch[url]);
}
else {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onload = function () {
var result = xhr.responseText;
self.fetch[url] = result;
bk.call(that, result);
};
xhr.send(null);
}
}
然后增加各页面的render方法, 代码如下
entry.js的代码
var entryPage = App.createPage("index", {
render: function (fn) {
this.fetch("/public/serve/html/entry.html", function (text) {
fn(text);
});
},
getDomObj: function (dom) {
this.attachDom(".btn-group", "btnGroup", dom)
.attachDom(".index-container", "container", dom)
.attachSlide("container", this.startFn, this.moveFn, this.endFn)
.attachTap("btnGroup", this.tapHandler, false);
},
tapHandler: function (ev) {
var target = ev.target;
var action = target.dataset.action;
switch (action) {
case "register":
app.render(registerPage);
break;
case "login":
app.render(loginPage);
break;
}
},
startFn: function (ev) {},
moveFn: function (ev) {},
endFn: function (ev) {
var speed = 1000 * ev.deltaX / ev.elapsed;
if (speed > 200) {
app.render(registerPage);
}
else if (speed < -200) {
app.render(loginPage);
}
}
});
login.js的代码
var loginPage = App.createPage("login", {
render: function (fn) {
this.fetch("/public/serve/html/login.html", function (text) {
fn(text);
});
},
getDomObj: function (dom) {
this.attachDom("[data-action='back']", "backBtn", dom)
.attachDom(".login-form", "form", dom)
.attachDom(".login-container", "container", dom)
.attachSlide("container", this.startFn, this.moveFn, this.endFn)
.attachTap("backBtn", this.tapBackHandler, false)
.attachEvent("form", "submit", this.formSubmitHandler, false);
},
tapBackHandler: function (ev) {
app.render(entryPage);
},
formSubmitHandler: function (ev) {
ev.preventDefault();
var form = ev.target;
var name = form.name.value;
var password = form.password.value;
app.render(goalPage);
},
startFn: function (ev) {},
moveFn: function (ev) {},
endFn: function (ev) {
var speed = 1000 * ev.deltaX / ev.elapsed;
if (speed > 200) {
app.render(entryPage);
}
}
});
register.js的代码
var registerPage = App.createPage("register", {
render: function (fn) {
this.fetch("/public/serve/html/register.html", function (text) {
fn(text);
});
},
getDomObj: function (dom) {
this.attachDom("[data-action='back']", "backBtn", dom)
.attachDom(".register-form", "form", dom)
.attachDom(".register-container", "container", dom)
.attachSlide("container", this.startFn, this.moveFn, this.endFn)
.attachTap("backBtn", this.tapBackHandler, false)
.attachEvent("form", "submit", this.submitHandler, false);
},
tapBackHandler: function (ev) {
app.render(entryPage);
},
submitHandler: function (ev) {
ev.preventDefault();
var form = ev.target;
var name = form.name.value;
var password = form.password.value;
var agree = form.agree.checked;
if (agree) {
app.render(goalPage);
}
},
startFn: function (ev) {},
moveFn: function (ev) {},
endFn: function (ev) {
var speed = 1000 * ev.deltaX / ev.elapsed;
if (speed > 200) {
app.render(entryPage);
}
}
});
goal.js的代码
var goalPage = App.createPage("goal", {
render: function (fn) {
this.fetch("/public/serve/html/goal.html", function (text) {
fn(text);
});
},
getDomObj: function () {}
});
index.js的代码
var app = new App();
app.render(entryPage);
总结: 这一篇建立一个最基础的本地服务器,后续还会根据上面的添加修改些小功能。不过不会引入太多的功能, 只会引入与前端展示有关的特性。 有了前后端,因此一个项目的结构就基本成型了,然后顺势的调整项目目录, 将页面结构重构, 减少第一次进入页面的数据加载量, 个人还是建议把页面结构彻底的分离开,每个js放置一个Page对象,真正等到项目上线的时候使用构建工具来合成及压缩。
后续更新:下一篇介绍history的api, 并将其引入App对象中,这样页面切换可以通过前进后退键了, 这样就有三个手段切换页面了。切换手段齐全了, 后续几篇接下来开始增加页面切换的效果, 让页面更加生动。
原生开发移动web单页面(step by step)1——传统页面的开发
原生开发移动web单页面(step by step)2——Page对象
原生开发移动web单页面(step by step)3——App对象
原生开发移动web单页面(step by step)4——tap事件与slide事件
原生开发移动web单页面(step by step)6——history api应用