JavaScript >> Javascript 文檔 >  >> Tags >> APP

製作沒有框架的單頁應用程序

單頁應用程序 (SPA) 背後的理念是創造一種流暢的瀏覽體驗,就像在原生桌面應用程序中發現的那樣。頁面的所有必要代碼只加載一次,其內容通過 JavaScript 動態更改。如果一切正常,則頁面不應重新加載,除非用戶手動刷新它。

有許多用於單頁應用程序的框架。首先我們有 Backbone,然後是 Angular,現在是 React。不斷學習和重新學習東西需要大量的工作(更不用說必須支持你在一個早已被遺忘的框架中編寫的舊代碼)。在某些情況下,例如當您的應用程序想法不太複雜時,實際上不使用任何外部框架來創建單頁應用程序並不難。這是怎麼做的。

理念

我們不會使用框架,但我們 正在使用兩個 - 用於 DOM 操作和事件處理的 jQuery,以及用於模板的 Handlebars。如果您希望更小,您可以輕鬆省略這些,但我們將使用它們來提高生產力。他們將在當今流行的客戶端框架被遺忘很久之後出現在這裡。

我們將要構建的應用程序從 JSON 文件中獲取產品數據,並通過使用 Handlebars 渲染產品網格來顯示它。初始加載後,我們的應用程序將停留在相同的 URL 上並監聽 hash 的變化 hashchange 的一部分 事件。要在應用程序中導航,我們只需更改哈希值。這還有一個額外的好處,就是瀏覽器歷史記錄無需我們額外的努力就可以正常工作。

設置

如您所見,我們的項目文件夾中沒有太多內容。我們有常規的網絡應用程序設置 - HTML、JavaScript 和 CSS 文件,附帶一個 products.json,其中包含有關我們商店中產品的數據和一個包含產品圖像的文件夾。

產品 JSON

.json 文件用於為我們的 SPA 存儲有關每個產品的數據。該文件可以很容易地被服務器端腳本替換,以從真實數據庫中獲取數據。

products.json

[
  {
    "id": 1,
    "name": "Sony Xperia Z3",
    "price": 899,
    "specs": {
      "manufacturer": "Sony",
      "storage": 16,
      "os": "Android",
      "camera": 15
    },
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique ipsum in efficitur pharetra. Maecenas luctus ante in neque maximus, sed viverra sem posuere. Vestibulum lectus nisi, laoreet vel suscipit nec, feugiat at odio. Etiam eget tellus arcu.",
    "rating": 4,
    "image": {
      "small": "/images/sony-xperia-z3.jpg",
      "large": "/images/sony-xperia-z3-large.jpg"
    }
  },
  {
    "id": 2,
    "name": "Iphone 6",
    "price": 899,
    "specs": {
      "manufacturer": "Apple",
      "storage": 16,
      "os": "iOS",
      "camera": 8
    },
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique ipsum in efficitur pharetra. Maecenas luctus ante in neque maximus, sed viverra sem posuere. Vestibulum lectus nisi, laoreet vel suscipit nec, feugiat at odio. Etiam eget tellus arcu.",
    "rating": 4,
    "image": {
      "small": "/images/iphone6.jpg",
      "large": "/images/iphone6-large.jpg"
    }
  }
]

HTML

在我們的 html 文件中,我們有幾個 div 共享同一個類“頁面”。這些是我們的應用程序可以顯示的不同頁面(或在 SPA 中稱為狀態)。但是,在頁面加載時,所有這些都通過 CSS 隱藏,需要 JavaScript 來顯示。這個想法是一次只能看到一頁,我們的腳本決定它是哪一頁。

index.html

<div class="main-content">

    <div class="all-products page">

        <h3>Our products</h3>

        <div class="filters">
            <form>
                Checkboxes here
            </form>
        </div>

    <ul class="products-list">
      <script id="products-template" type="x-handlebars-template">​
        {{#each this}}
          <li data-index="{{id}}">
            <a href="#" class="product-photo"><img src="{{image.small}}" height="130" alt="{{name}}"/></a>
            <h2><a href="#"> {{name}} </a></h2>
            <ul class="product-description">
              <li><span>Manufacturer: </span>{{specs.manufacturer}}</li>
              <li><span>Storage: </span>{{specs.storage}} GB</li>
              <li><span>OS: </span>{{specs.os}}</li>
              <li><span>Camera: </span>{{specs.camera}} Mpx</li>
            </ul>
            <button>Buy Now!</button>
            <p class="product-price">{{price}}$</p>
            <div class="highlight"></div>
          </li>
        {{/each}}
      </script>

    </ul>

    </div>

    <div class="single-product page">

        <div class="overlay"></div>

        <div class="preview-large">
            <h3>Single product view</h3>
            <img src=""/>
            <p></p>

            <span class="close">&times;</span>
        </div>

    </div>

    <div class="error page">
        <h3>Sorry, something went wrong :(</h3>
    </div>

</div>

我們有三個頁面:所有產品 (產品列表),單一產品 (單個產品頁面)和錯誤 .

所有產品 頁面由標題、包含用於過濾的複選框的表單和具有“產品列表”類的

    標記組成。此列表是使用存儲在 products.json 中的數據通過把手生成的,為 json 中的每個條目創建一個
  • 。結果如下:

    單一產品用於顯示僅關於一種產品的信息。它是空的並且在頁面加載時隱藏。當達到適當的哈希地址時,它會填充產品數據並顯示出來。

    錯誤頁面僅包含一條錯誤消息,用於在您到達錯誤地址時通知您。

    JavaScript 代碼

    首先,讓我們快速預覽一下這些功能及其作用。

    script.js

    $(function () {
    
        checkboxes.click(function () {
            // The checkboxes in our app serve the purpose of filters.
            // Here on every click we add or remove filtering criteria from a filters object.
    
            // Then we call this function which writes the filtering criteria in the url hash.
            createQueryHash(filters);
        });
    
        $.getJSON( "products.json", function( data ) {
            // Get data about our products from products.json.
    
            // Call a function that will turn that data into HTML.
            generateAllProductsHTML(data);
    
            // Manually trigger a hashchange to start the app.
            $(window).trigger('hashchange');
        });
    
        $(window).on('hashchange', function(){
            // On every hash change the render function is called with the new hash.
            // This is how the navigation of our app happens.
            render(decodeURI(window.location.hash));
        });
    
        function render(url) {
            // This function decides what type of page to show 
            // depending on the current url hash value.
        }
    
        function generateAllProductsHTML(data){
            // Uses Handlebars to create a list of products using the provided data.
            // This function is called only once on page load.
        }
    
        function renderProductsPage(data){
            // Hides and shows products in the All Products Page depending on the data it recieves.
        }
    
        function renderSingleProductPage(index, data){
            // Shows the Single Product Page with appropriate data.
        }
    
        function renderFilterResults(filters, products){
            // Crates an object with filtered products and passes it to renderProductsPage.
            renderProductsPage(results);
        }
    
        function renderErrorPage(){
            // Shows the error page.
        }
    
        function createQueryHash(filters){
            // Get the filters object, turn it into a string and write it into the hash.
        }
    
    });
    

    請記住,SPA 的概念是在應用程序運行時不進行任何負載。這就是為什麼在初始頁面加載之後,我們希望留在同一頁面上,我們需要的所有內容都已被服務器獲取。

    但是,我們仍然希望能夠訪問應用程序中的某個位置,例如,複製 url 並將其發送給朋友。如果我們從不更改應用程序的地址,他們只會以最初的方式獲取應用程序,而不是您想與他們分享的內容。為了解決這個問題,我們將有關應用程序狀態的信息寫入 url 作為#hash。哈希不會導致頁面重新加載,並且易於訪問和操作。

    在每個 hashchange 上,我們稱之為:

    function render(url) {
    
            // Get the keyword from the url.
            var temp = url.split('/')[0];
    
            // Hide whatever page is currently shown.
            $('.main-content .page').removeClass('visible');
    
            var map = {
    
                // The Homepage.
                '': function() {
    
                    // Clear the filters object, uncheck all checkboxes, show all the products
                    filters = {};
                    checkboxes.prop('checked',false);
    
                    renderProductsPage(products);
                },
    
                // Single Products page.
                '#product': function() {
    
                    // Get the index of which product we want to show and call the appropriate function.
                    var index = url.split('#product/')[1].trim();
    
                    renderSingleProductPage(index, products);
                },
    
                // Page with filtered products
                '#filter': function() {
    
                    // Grab the string after the '#filter/' keyword. Call the filtering function.
                    url = url.split('#filter/')[1].trim();
    
                    // Try and parse the filters object from the query string.
                    try {
                        filters = JSON.parse(url);
                    }
                    // If it isn't a valid json, go back to homepage ( the rest of the code won't be executed ).
                    catch(err) {
                        window.location.hash = '#';
                    }
    
                    renderFilterResults(filters, products);
                }
    
            };
    
            // Execute the needed function depending on the url keyword (stored in temp).
            if(map[temp]){
                map[temp]();
            }
            // If the keyword isn't listed in the above - render the error page.
            else {
                renderErrorPage();
            }
    
        }
    

    該函數會考慮我們哈希的開頭字符串,決定需要顯示哪個頁面並調用相應的函數。

    例如,如果哈希是 '#filter/{"storage":["16"],"camera":["5"]}',我們的代碼字是 '#filter'。現在渲染函數知道我們想要查看一個包含過濾後的產品列表的頁面,並將導航到它。其餘的哈希值將被解析為一個對象,並顯示一個包含過濾產品的頁面,從而更改應用程序的狀態。

    這僅在啟動時調用一次,並通過把手將我們的 JSON 轉換為實際的 HTML5 內容。

    function generateAllProductsHTML(data){
    
        var list = $('.all-products .products-list');
    
        var theTemplateScript = $("#products-template").html();
        //Compile the template​
        var theTemplate = Handlebars.compile (theTemplateScript);
        list.append (theTemplate(data));
    
        // Each products has a data-index attribute.
        // On click change the url hash to open up a preview for this product only.
        // Remember: every hashchange triggers the render function.
        list.find('li').on('click', function (e) {
          e.preventDefault();
    
          var productIndex = $(this).data('index');
    
          window.location.hash = 'product/' + productIndex;
        })
      }
    

    這個函數接收一個只包含我們想要展示的產品的對象並顯示出來。

    function renderProductsPage(data){
    
        var page = $('.all-products'),
          allProducts = $('.all-products .products-list > li');
    
        // Hide all the products in the products list.
        allProducts.addClass('hidden');
    
        // Iterate over all of the products.
        // If their ID is somewhere in the data object remove the hidden class to reveal them.
        allProducts.each(function () {
    
          var that = $(this);
    
          data.forEach(function (item) {
            if(that.data('index') == item.id){
              that.removeClass('hidden');
            }
          });
        });
    
        // Show the page itself.
        // (the render function hides all pages so we need to show the one we want).
        page.addClass('visible');
    
      }
    

    展示單品預覽頁面:

    function renderSingleProductPage(index, data){
    
        var page = $('.single-product'),
          container = $('.preview-large');
    
        // Find the wanted product by iterating the data object and searching for the chosen index.
        if(data.length){
          data.forEach(function (item) {
            if(item.id == index){
              // Populate '.preview-large' with the chosen product's data.
              container.find('h3').text(item.name);
              container.find('img').attr('src', item.image.large);
              container.find('p').text(item.description);
            }
          });
        }
    
        // Show the page.
        page.addClass('visible');
    
      }
    

    獲取所有產品,根據我們的查詢過濾它們並返回一個帶有結果的對象。

    function renderFilterResults(filters, products){
    
          // This array contains all the possible filter criteria.
        var criteria = ['manufacturer','storage','os','camera'],
          results = [],
          isFiltered = false;
    
        // Uncheck all the checkboxes.
        // We will be checking them again one by one.
        checkboxes.prop('checked', false);
    
        criteria.forEach(function (c) {
    
          // Check if each of the possible filter criteria is actually in the filters object.
          if(filters[c] && filters[c].length){
    
            // After we've filtered the products once, we want to keep filtering them.
            // That's why we make the object we search in (products) to equal the one with the results.
            // Then the results array is cleared, so it can be filled with the newly filtered data.
            if(isFiltered){
              products = results;
              results = [];
            }
    
            // In these nested 'for loops' we will iterate over the filters and the products
            // and check if they contain the same values (the ones we are filtering by).
    
            // Iterate over the entries inside filters.criteria (remember each criteria contains an array).
            filters[c].forEach(function (filter) {
    
              // Iterate over the products.
              products.forEach(function (item){
    
                // If the product has the same specification value as the one in the filter
                // push it inside the results array and mark the isFiltered flag true.
    
                if(typeof item.specs[c] == 'number'){
                  if(item.specs[c] == filter){
                    results.push(item);
                    isFiltered = true;
                  }
                }
    
                if(typeof item.specs[c] == 'string'){
                  if(item.specs[c].toLowerCase().indexOf(filter) != -1){
                    results.push(item);
                    isFiltered = true;
                  }
                }
    
              });
    
              // Here we can make the checkboxes representing the filters true,
              // keeping the app up to date.
              if(c && filter){
                $('input[name='+c+'][value='+filter+']').prop('checked',true);
              }
            });
          }
    
        });
    
        // Call the renderProductsPage.
        // As it's argument give the object with filtered products.
        renderProductsPage(results);
      }
    

    顯示錯誤狀態:

    function renderErrorPage(){
        var page = $('.error');
        page.addClass('visible');
      }
    

    將過濾器對象字符串化並將其寫入哈希中。

    function createQueryHash(filters){
    
        // Here we check if filters isn't empty.
        if(!$.isEmptyObject(filters)){
          // Stringify the object via JSON.stringify and write it after the '#filter' keyword.
          window.location.hash = '#filter/' + JSON.stringify(filters);
        }
        else{
          // If it's empty change the hash to '#' (the homepage).
          window.location.hash = '#';
        }
    
      }
    

    結論

    當您希望為您的項目提供更動態和流暢的感覺時,單頁應用程序是完美的選擇,並且借助一些巧妙的設計選擇,您可以為訪問者提供精美、愉快的體驗。


下一篇
No
Tutorial JavaScript 教程
  1. 訪客模式

  2. 課堂日誌 - JavaScript 和 Rails 項目

  3. 如何創建一個正則表達式來查找和替換從 [] 到 Array 的所有 jsdoc 數組語法? [關閉]

  4. 自定義 React 模板的樣板

  5. 如何在 JavaScript 中反轉正則表達式?

  6. [教程] 如何創建 Web 組件?

  7. Context2D 畫布問題。但它是 Webgl 嗎?

  1. 將二維單詞數組轉換為單個數組 [關閉]

  2. 閾值圖像顏色 - Base64

  3. 反應路由器不顯示瀏覽器歷史記錄

  4. 如果你刪除了一個 DOM 元素,任何以該元素開始的事件是否會繼續冒泡?

  5. 5 分鐘了解 Angular 指令

  6. 為什麼深色主題比普通主題更好?

  7. 像專業人士一樣處理 React 組件中的錯誤

  1. CYOMS - 製作您自己的 Minecraft 服務器

  2. 在本機反應中使用自定義字體

  3. 構建一個文本編輯器,比如 1999 年的 Remirror

  4. 關於 React 組件你可能不知道的事情