Currently, Frameworks such as React, Vue and Angular all support single-page applications. Recently, I have been reviewing my knowledge about the single-page applications built by Knockout + require + Director when I graduated from university. So writing a review of previous single-page programs may be helpful in understanding single-page programs today.

Pre-briefing

From the old days of multi-page programs, in the days of JS, HTML, and jquery, the route jumped through links, and the header and footer of each page would be written once on each page. I want to create a single page program. How many questions are there?


  • Dynamic rendering part of the area content needs to be able to control through JS
  • Monitor route redirects
  • Js controls the jump of dynamic routing to render a specific page, and how to deal with HTML rendering and JS execution. Because we want to bind a page and data model through KO, we also need to add ko binding page data data processing here. Therefore, js files are processed in a manner similar to the React/Vue life cycle.

rquire

Require.js was created to answer two questions:

  1. Asynchronous loading of JS files to avoid losing response of web pages;
  2. Manage dependencies between modules for easy code writing and maintenance.

We use require here to control HTML and JS files via require. Require is designed to control JS files, so how do you control HTML? You can control HTML via the require plugin text.

knockout

Knockout is the granddaddy of the MVVM framework. Realize the function of two-way data binding.

Knockout is an excellent JavaScript library that can help you create a rich text user interface with good display and editing functionality using only a clean, underlying data model.

KO is easy to implement and easy to maintain any time your local UI content needs to be updated automatically (for example, depending on changes in user behavior or external data sources).

The main functionality we are using here is the Knockout Tempate functionality and the KO data binding functionality used when writing business functionality. The Template binding renders data to the page using the template. Template bindings are handy for building pages with nested structures.

Note image source: www.cnblogs.com/tangge/p/10…

director

Director.js is a client-side route registry/resolver that uses the # symbol to organize different URL paths and match different callback methods based on different URL paths without refreshing the page. Popular point is what kind of path do what kind of things.

Director is mainly used here to call JS code for switching single page routing.

require

As I said, we need to control HTML and CSS through require.

Initialize the require

<script src="js/require.js" data-main="js/main"></script>
Copy the code

The data-main property specifies the main module of the web application. In this case, the main.js file under the js directory will be the first to be loaded by require.js. Since require.js’s default file suffix is js, you can simply write main.js as main.

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Document</title>
</head>
<body>
  <main id="main"></main>
  <script data-main="/Scripts/framework/main" src="/Scripts/lib/require.js"></script>
</body>
</html>
Copy the code

In our code, after loading the require file, we will execute the main.js file.

The main js file

var paths = {
/ *TODO: register all AMD modules by providing CamelCase aliases, exceptions are RequireJS plugins and named AMD modules, whose names are fixed */
/* follow files dictionary order */
'jquery': 'Scripts/lib/jquery'.'Routes': 'Scripts/framework/routes'.'knockout': 'Scripts/lib/knockout'.//framework
'Router': 'Scripts/lib/director'.'WebPageContrl': 'Scripts/framework/webPageContrl'.'AppRouter': 'Scripts/framework/router'.'Error-js': 'Scripts/app/Error'.'Error-html': 'templates/Error-html.html'."knockout-amd-helpers": "Scripts/lib/knockout-amd-helpers"."text": "Scripts/lib/text".//bootstrap
'Custom': 'Scripts/lib/custom'.'Bootstrap': 'Scripts/lib/bootstrap.min'.//Customer
'CustomerIntroduction-html': 'templates/customer/CustomerIntroduction.html'.'CustomerIntroduction-js': 'Scripts/app/customer/CustomerIntroduction'.//require
'RequireIntroduction-html': 'templates/require/RequireIntroduction.html'."RequireIntroduction-js": 'Scripts/app/require/RequireIntroduction'.'RequireCode-html': 'templates/require/RequireCode.html'."RequireCode-js": 'Scripts/app/require/RequireCode'.//Javascript
'UnknowJavascriptSecond-html': 'templates/javascript/UnknowJavascriptSecond.html'.'UnknowJavascriptSecond-js': 'Scripts/app/javascript/UnknowJavascriptSecond'};var baseUrl = '/';

require.config({
	baseUrl: baseUrl,
	paths: paths,

	shim: {
		/ *TODO: provide all needed shims for non-AMD modules */
		'Router': {
			exports: 'Router'
		},
		'Custom': {
      exports: 'Custom'
		},
		'Custom': ['Bootstrap'].'Bootstrap': ['jquery']}});require(["jquery"."RequireIntroduction-js"."text! RequireIntroduction-html"].function ($, module, html) {
        console.log("Start test require html!");
        $('#main').html(html);
        console.log("Start test require js!");
        module.TestRequireJs(); });Copy the code

This file is probably a little bit too much of the other stuff, so let’s just focus on the main points here.

require.config

Using the require.config() method, we can customize the loading behavior of the module.

Require.config () is written at the head of the main module (main.js). The parameter is an object whose Paths property specifies the loading path for each module.

Here we define many files with long file paths through aliases.

Render HTML & execute JS

require(["jquery"."RequireIntroduction-js"."text! RequireIntroduction-html"].function ($, module, html) {
        console.log("Start test require html!");
        $('#main').html(html);
        console.log("Start test require js!");
        module.TestRequireJs(); });Copy the code

Here you get the HTML and JS files of the current page RequireIntroduction. Render HTML using JQUERY HTML methods. Get the JS module and call the JS method.

Take a look at the HTML and JS code

<! --RequireIntroduction-html-->
<div>
  require introduction
</div>
Copy the code
// RequireIntroduction-js
define(function () {
    function RequireIntroductionViewModel() {
        var self = this;
        
        self.TestRequireJs = () = > {
            console.log('testRequireJS'); }}return new RequireIntroductionViewModel();
});
Copy the code

knockout

Download the required files knockout.js and knockout-amd-helpers.js. The main use of knockout-amd-helpers.js in this chapter is that KNOCKOUT can render the entire HTML file directly when rendering the template. Instead of defining the template in the current Web page.

Now we want to render the page using the Knockout Template.

Modified index. HTML

<main id="main"
    data-bind="template: { name: 'RequireIntroduction-html', data: require('RequireIntroduction-js').data, afterRender: require('RequireIntroduction-js').afterRender }">
  </main>
  <script data-main="/Scripts/framework/main" src="/Scripts/lib/require.js"></script>
Copy the code

Modify the main js

var paths = {
/ *TODO: register all AMD modules by providing CamelCase aliases, exceptions are RequireJS plugins and named AMD modules, whose names are fixed */
/* follow files dictionary order */
'jquery': 'Scripts/lib/jquery'.'Routes': 'Scripts/framework/routes'.'knockout': 'Scripts/lib/knockout'.//framework
'Router': 'Scripts/lib/director'.'WebPageContrl': 'Scripts/framework/webPageContrl'.'AppRouter': 'Scripts/framework/router'.'Error-js': 'Scripts/app/Error'.'Error-html': 'templates/Error-html.html'."knockout-amd-helpers": "Scripts/lib/knockout-amd-helpers"."text": "Scripts/lib/text".//bootstrap
'Custom': 'Scripts/lib/custom'.'Bootstrap': 'Scripts/lib/bootstrap.min'.//Customer
'CustomerIntroduction-html': 'templates/customer/CustomerIntroduction.html'.'CustomerIntroduction-js': 'Scripts/app/customer/CustomerIntroduction'.//require
'RequireIntroduction-html': 'templates/require/RequireIntroduction.html'."RequireIntroduction-js": 'Scripts/app/require/RequireIntroduction'.'RequireCode-html': 'templates/require/RequireCode.html'."RequireCode-js": 'Scripts/app/require/RequireCode'.//Javascript
'UnknowJavascriptSecond-html': 'templates/javascript/UnknowJavascriptSecond.html'.'UnknowJavascriptSecond-js': 'Scripts/app/javascript/UnknowJavascriptSecond'};var baseUrl = '/';

require.config({
	baseUrl: baseUrl,
	paths: paths,

	shim: {
		/ *TODO: provide all needed shims for non-AMD modules */
		'Router': {
			exports: 'Router'
		},
		'Custom': {
      exports: 'Custom'
		},
		'Custom': ['Bootstrap'].'Bootstrap': ['jquery']}});require(["knockout"."RequireIntroduction-js"."knockout-amd-helpers"."text"].function (ko, RequireIntroduction) {
	ko.bindingHandlers.module.baseDir = "modules";

	//fruits/vegetable modules have embedded template
	ko.bindingHandlers.module.templateProperty = "embeddedTemplate";
	ko.applyBindings(RequireIntroduction);
});
Copy the code

From the brief introduction, I have learned that I need a Knockout template for HTML rendering, but instead of defining the template, I use the following code:

require(["knockout"."RequireIntroduction-js"."knockout-amd-helpers"."text"].function (ko, RequireIntroduction) {
	ko.bindingHandlers.module.baseDir = "modules";

	//fruits/vegetable modules have embedded template
	ko.bindingHandlers.module.templateProperty = "embeddedTemplate";
	ko.applyBindings(RequireIntroduction);
});
Copy the code

Because knockout-AMD-helpers is used here

The plug-in is intended to be a lightweight and flexible solution for using AMD modules in Knockout.js. This library was originally designed for require.js or curl. It provides two main functions:

  1. Enhance the default template engine to allow it to load external templates using text plug-ins for THE AMD loader. This allows you to create templates in separate HTML files and pull them in as needed (ideally, in production templates are included in optimization files).
  2. Create a Module binding that provides a flexible way to load data from an AMD module and bind it to an external template, an anonymous/inline template, or a template defined in a property of the module itself.

What is the effect of the above code execution?

When we write the code, this call will load the HTML file corresponding to the current key from the require.

By this point, require has been associated with knockout. Knockout does template rendering and require does file control.

Page rendering

Review the previous question

Then the main problem is: monitoring route changes, in the system code can dynamically handle the current path corresponding to the HTML/JS resources. The execution of the JS code is then split by KO binding and lifecycle.

Integrate director to complete a single page program

System-level code handling webPageContrl

/* * @Description: * @Author: rodchen * @Date: 2021-07-24 12:08:10 * @LastEditTime: 2021-07-24 21:33:46 * @LastEditors: rodchen */
define(['knockout'.'jquery'.'Router'.'Custom'].function (ko, $, Router) {
	var initialRun = true;

	function isEndSharp() { // url end with #
		if(app.lastUrl ! =""&& location.toLocaleString().indexOf(app.lastUrl) ! = -1 && location.toLocaleString().indexOf(The '#') != -1 && location.hash == "") {
			return true;
		}

		return false;
	}

	var app = {
    // Call the current method each time the route is switched
	  initJS: function (pageName) {
			require([pageName + '-js'].function (page) {
					app.init(pageName, page);
			});
	  },
		init: function(pageName, pageData) {
			if (isEndSharp()) {
				return;
			}

      // execute the init method of the js module
			pageData.init(); 

      // data bound by ko. The data bound source is the data module in the JS module
			app.page({
			    name: pageName,				// Page identifier of the route
			    data: pageData
			}); 

      // For the first time, ko binding
			if (initialRun) {
				ko.applyBindings(app, document.getElementsByTagName('html') [0]); 
				initialRun = false; }},page: ko.observable({
			name: ' '.data: {
				init: function() {}}}),// Knockout Template is the callback function that has been successfully loaded
		afterRender: function() {
			if(app.page().data.afterRender) { app.page().data.afterRender(); }}};return app;
});
Copy the code

Director to deal with

/* * // router.js * @Description: * @Author: rodchen * @Date: 2021-07-24 12:08:10 * @LastEditTime: 2021-07-24 19:59:11 * @LastEditors: rodchen */
define(['WebPageContrl'.'Routes'.'Router'].function (WebPageContrl, Routes, Router) {
	var routes = {};

	$.each(Routes, function(key, value) {
		var values = value.split(' ');
		var pageName = values[0];
    
		routes[key] = function() {
		  WebPageContrl.initJS(pageName);  // The callback function is routed through the director, and then the initJS method of the system file is executed
		};
	});

	var router = new Router(routes).configure({
		notfound: function() {
			routes['/error404/:code'] (404); }});var urlNotAtRoot = window.location.pathname && (window.location.pathname ! ='/');

	if (urlNotAtRoot) {
		router.init();
	} else {
		router.init('/');
	}

	return router;
});
Copy the code
// Routes

define({
	'/error404/:code': 'Error /'.'/': 'CustomerIntroduction /'.' ': 'CustomerIntroduction /'.//Customer
	'/CustomerIntroduction': 'CustomerIntroduction /'.//Require
	'/RequireIntroduction': 'RequireIntroduction /'.'/RequireCode': 'RequireCode /'.//Javascript
	'/UnknowJavascriptSecond': 'UnknowJavascriptSecond /'
})
Copy the code

To illustrate the whole process

Code address: github.com/rodchen-kin…

// Code execution is performed via http-server because it is a relative path and needs to be attached to the service NPM install global HTTP-serverCopy the code

With reference to

    1. www.ruanyifeng.com/blog/2012/1…
    1. www.cnblogs.com/tangge/p/10…
    1. www.cnblogs.com/Showshare/p…