使用原生 JavaScript 构建单页应用程序
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
我经常会遇到各种框架之争。很多时候我是旁观者,偶尔也会参与其中,偶尔还会主动挑起争论。这些争论中最常见的论点是:哪种方法更容易实现,或者哪种方法需要编写的代码更少。但我很少看到有人讨论用原生JavaScript编写代码,并更好地组织项目结构,而不是依赖框架。令我震惊的是,很多开发者除了自己喜欢的框架之外,对其他技术一无所知,甚至害怕编写纯 JavaScript 代码。如果你决定以编程为生,怎么能不了解你所使用的插件的基本构建模块呢?
很多优秀的开发者过去都讨论过这个问题。我最喜欢的几篇文章是《零框架宣言》(Zero Framework Manifesto)和《看,没有框架》(Look Ma, No Frameworks)。许多框架都擅长通过在其网站或开发者博客上展示其主要功能或预期优势来进行市场推广,然而,我很少看到有人分享如何使用原生 JavaScript 构建单页应用(SPA)。因此,我决定将我的个人网站重构为一个不使用任何框架的 SPA。我希望这篇文章能为你提供一个良好的开端,帮助你在不使用任何框架的情况下独立构建应用。本文中提到的所有代码都可以在vinay20045.github.io 代码库中找到,而本网站本身也可以作为一个在线演示。
在重构
之前,我的网站是一个典型的用 PHP 编写的博客。每个页面请求都需要往返服务器获取所有 HTML 内容和资源,网站还包含一个管理控制台等等。在重构过程中,我考虑的一些因素包括……
- 无需每篇文章都加载页面,也就是说,它应该是一个单页应用(SPA)。
- 文章需使用 Markdown 语法编写。
- 博客应该只用 HTML+CSS+JS 编写。
- 托管将在 GitHub Pages 或 AWS S3 上进行。
- 它必须支持移动设备。
基本结构
在开发任何应用程序时,首先要关注的就是代码的组织结构。这包括从文件夹结构和命名规范到声明和定义等所有内容。我见过很多开发者争论两个换行符还是一个换行符,但他们却能接受将业务逻辑放在视图或模板中。总之,一旦你在一个项目中实现了这一点,它就相当于一个样板代码,可以轻松地复制到你未来的项目中并进行扩展。
博客应用程序的基本结构如下所示……
|-- assets
| |-- css -- All site styles go here
| |-- images -- All images used in the templates or page shell go here
| `-- js
| |-- config.js -- Environment specific config file
| |-- init.js -- Contains all instructions on load
| |-- controllers -- Business logic and view manipulation functions
| |-- templates -- context based reusable snippets of HTML
| |-- utils -- All internal and 3rd party libraries
| `-- views -- Views exposed to the user
|-- index.html -- Page shell. Acts like a container. Actual content is populated based on route
|-- posts -- All posts markdown files go here
`-- uploads -- All assets used in posts go here
路由:
为了方便深度链接、书签添加和提升搜索引擎优化 (SEO),设置合适的路由至关重要。路由技术有很多种,但基于哈希的路由效果显著且易于实现。应用程序加载时,路由函数会注册到哈希更改事件中。
路由函数是utils 库的一部分,它的代码如下所示……
router: function(route, data){
route = route || location.hash.slice(1) || 'home';
var temp = route.split('?');
var route_split = temp.length;
var function_to_invoke = temp[0] || false;
if(route_split > 1){
var params = extract_params(temp[1]);
}
//fire away...
if(function_to_invoke){
views[function_to_invoke](data, params);
}
}
extract_params函数看起来是这样的……
var extract_params = function(params_string){
var params = {};
var raw_params = params_string.split('&');
var j = 0;
for(var i = raw_params.length - 1; i >= 0; i--){
var url_params = raw_params[i].split('=');
if(url_params.length == 2){
params[url_params[0]] = url_params[1];
}
else if(url_params.length == 1){
params[j] = url_params[0];
j += 1;
}
else{
//param not readable. pass.
}
}
return params;
};
事件监听器已在 init.js 中注册……
window.addEventListener(
"hashchange",
function(){utils.router()} // the router is part of the utils library
);
剖析控制器:
控制器承载着业务逻辑。您可以使用其中的函数来操作视图。这些函数不会直接对用户暴露。它们只能访问模板和工具类库。它们可以由视图或其他控制器调用。
负责主页的控制器看起来像这样……
controllers.home_page = function(data, params){
var all_posts = JSON.parse(data);
var posts_to_show = 3;
var template_context = [];
for (var i = 0; i < posts_to_show; i++){
var post = all_posts[i];
var item = {
'link': '#post?'+post.post,
'title': post.post.replace(/-/g, ' '),
'snippet': post.snippet,
'published_on': post.added_on,
};
template_context.push(item);
}
//get recent posts
var recent_posts = templates.recent_posts(template_context);
//get hello text
var hello_text = templates.hello_text();
var final_content = hello_text + recent_posts;
utils.render(
'page-content',
final_content
);
};
剖析模板:
模板包含实际页面内容的 HTML 标记。当可以使用函数根据传递的上下文生成所需的 HTML 时,模板有助于提高代码重用性。模板的所有功能都必须由调用它的控制器通过数据绑定和事件注册技术来提供。我唯一允许的例外是 href 属性。
首页“你好”部分的模板是……
templates.hello_text = function(data){
var content = `
<div id="hello_text">
<h2>Hello...</h2>
<img src="assets/images/Vinay.jpg" align="left" style="width:70px;">
<p>
Thank you for visiting my blog. I am Vinay Kumar NP. I am a passionate techie...
</p>
<p>
I am currently working on a <a href="http://www.int.ai/" target = "_BLANK">startup</a> of my own. I have previously worked in various engineering leadership positions at...
</p>
</div>
`;
return content;
};
剖析视图:
视图是直接暴露给用户的函数,也就是说,它们由路由器调用,并且是 URL 的一部分。视图函数和控制器之间没有其他区别。你也可以暴露控制器,但这可能会损害模块化。
所有文章页面的视图如下所示。它只是show_posts在通过 AJAX 调用获取文章索引文件后,将请求传递给加载控制器。
views.all_posts = function(data, params){
var api_stub = 'posts/index.json';
utils.request(
api_stub,
'show_all_posts',
'show_all_posts_error'
);
};
发起 API 请求
是所有单页应用(SPA)的终极目标。虽然我的博客不需要外部 API 调用机制(因为所有文章都托管在内部),但我还是编写了这个功能来演示这个概念。请求方法接收 API 存根、回调函数和参数,并触发请求。这也是utils 库的一部分。(请注意 CORS 问题。)
用于发起 API 调用的函数如下所示……
request: function(api_stub, success_callback, error_callback, callback_params){
api_stub = api_stub || '';
callback_params = callback_params || {};
controllers.show_loader('page-content');
var url = config.api_server + api_stub;
var x = new XMLHttpRequest();
x.onreadystatechange = function(){
if (x.readyState == XMLHttpRequest.DONE) {
if(x.status == 200){
controllers[success_callback](
x.responseText,
callback_params
);
}
else{
controllers[error_callback](
x.status,
callback_params
);
}
}
};
//other methods can be implemented here
x.open('GET', url, true);
x.send();
}
我还没来得及做对比测试,但初步来看,我的所有页面重绘都很快,几乎没有卡顿。至于首次加载时网络调用过多的问题,我计划为我的另一个项目开发一个基于 Python 的网站打包器,完成后我会发布出来。
瞧,是不是很简单?如果你还不相信,那就打开浏览器,克隆我的代码库,进行必要的修改(配置、模板等等),然后自己动手试试。我非常肯定,你不仅会开始构建自己的、无需框架的 JavaScript 应用,还会为开源世界贡献更多库……世界需要更多乐于分享的人 :)
我已经在所有主流浏览器(IE 除外)上测试过这段代码,运行似乎没有任何问题。在构建自己的应用程序时,请注意 JavaScript API 的兼容性(例如,我使用了反引号,它与一些旧版浏览器不兼容)。如果您发现代码中的任何错误或问题,请告诉我。
这篇文章最初发表在我的博客上。
文章来源:https://dev.to/vinay20045/building-a-single-page-application-with-vanilla-js
