iT邦幫忙

2021 iThome 鐵人賽

DAY 4
0
自我挑戰組

Vue.js 從零開始系列 第 4

Vue.js 從零開始:SPA怎麼改善SEO呢? MVC與關注點分離又是什麼?

  • 分享至 

  • xImage
  •  

上一篇講到SPA的缺點,Vue是用JvaScript載入後台的數據,並且動態產生元件,SEO只能抓取HTML內容,導致無法抓到該有的數據,因此有團隊開發出Nuxt.js這套前端框架,框架或工具都是因應某個需要被解決的問題而生的。


Nuxt.js 是什麼?

Nuxt.js是以Vue為基礎所建構的框架,非Vue官方所開發的,能做到SPA的開發模式,另一方面又可以接換到SSR模式,改善SEO無法爬蟲的缺點。

Nuxt SSR 運作原理

https://ithelp.ithome.com.tw/upload/images/20210917/20118347FRh3CNtaSB.png

當使用者第一次載入到網頁時,nuxt server會先解析pages.vue檔或是compontents.vue檔,之後回傳給nuxt server,整理出一份HTML檔案到client端讓畫面顯示出來,這個部分以前都是使用SSR模式在跑流程,之後開始轉用SPA模式,點擊nuxt-link,都不會讓頁面跳頁,除非點擊單純的a連結,點擊a連結將會在種跑一次SSR模式,大大改善SEO的缺點,這樣的好處是一開始就有html結構,讓搜尋爬蟲能找到資料,之後再開始切換SPA,使用者體驗有了,SEO問題也解決了,但目前Vue3版本還無法支援


MVC 與關注點分離(SOC)

以前的觀念,就是把所有程式都往HTML頁面塞,這種程式碼稱為義大利麵式程式碼(Spaghetti code),隨著專案的需求越來越大,讓閱讀與開發變得非常困難,在開發領域裡,有個關注點分離的設計原則(Separation of Concerns),意思就是程式需要拆解成不同區塊,各自分工合作,在撰寫程式碼時,要同時思考後續的維護性。
有了以上的原則後,就可以將模型檢視控制器(MVC)拆解成三個區塊:
https://ithelp.ithome.com.tw/upload/images/20210917/20118347TAXYs7KiOa.png

  • Model:模型、資料層,資料狀態。
  • View:畫面、視圖、表現層,前端樣板。
  • Controller:控制器、邏輯層,處理資料邏輯,舉例像是電視遙控器,使用者按下按鈕,遙控器發出請求指令。

請觀察下列的程式碼架構

非關注點分離:

JavaScript:

let productData = []

document.getElementById('addProduct').addEventListener('click', (e) => {
  const timeStamp = Math.floor(Date.now());
  if (document.getElementById('title').value.trim() !== '') {
    productData.push({
      id: timeStamp,
      title: document.getElementById('title').value.trim(),
      origin_price: parseInt(document.getElementById('origin_price').value) || 0,
      price: parseInt(document.getElementById('price').value) || 0,
      is_enabled: false,
    })
    let str = '';
    productData.forEach((item) => {
      str += `
      <tr>
        <td>${item.title}</td>
        <td width="120">
          ${item.origin_price}
        </td>
        <td width="120">
          ${item.price}
        </td>
        <td width="100">
          <div class="form-check form-switch">
            <input class="form-check-input" type="checkbox" id="is_enabled" ${item.is_enabled? 'checked': ''} data-action="complete" data-id="${item.id}">
            <label class="form-check-label" for="is_enabled">${item.is_enabled? '啟用' : '未啟用'}</label>
          </div>
        </td>
        <td width="120">
          <button type="button" class="btn btn-sm btn-danger move" data-action="remove" data-id="${item.id}"> 刪除 </button>
        </td>
      </tr>`;
    })
    document.getElementById('productList').innerHTML = str;
    document.getElementById('productCount').textContent = productData.length;

    document.getElementById('title').value = '';
    document.getElementById('origin_price').value = '';
    document.getElementById('price').value = '';
  }
});

document.getElementById('clearAll').addEventListener('click', (e) => {
  e.preventDefault();
  productData = [];

  let str = '';
  productData.forEach((item) => {
    str += `
    <tr>
      <td>${item.title}</td>
      <td width="120">
        ${item.origin_price}
      </td>
      <td width="120">
        ${item.price}
      </td>
      <td width="100">
        <div class="form-check form-switch">
          <input class="form-check-input" type="checkbox" id="is_enabled" ${item.is_enabled? 'checked': ''} data-action="complete" data-id="${item.id}">
          <label class="form-check-label" for="is_enabled">${item.is_enabled? '啟用' : '未啟用'}</label>
        </div>
      </td>
      <td width="120">
        <button type="button" class="btn btn-sm btn-danger move" data-action="remove" data-id="${item.id}"> 刪除 </button>
      </td>
    </tr>`;
  })
  document.getElementById('productList').innerHTML = str;
  document.getElementById('productCount').textContent = productData.length;
});

document.getElementById('productList').addEventListener('click', (e) => {
  const action = e.target.dataset.action;
  const id = e.target.dataset.id;
  if (action === 'remove') {
    let newIndex = 0;
    productData.forEach((item, key) => {
      if (id == item.id) {
        newIndex = key;
      }
    })
    productData.splice(newIndex, 1);

  } else if (action === 'complete') {
    productData.forEach((item) => {
      if (id == item.id) {
        item.is_enabled = !item.is_enabled;
      }
    })
  }
  let str = '';
  productData.forEach((item) => {
    str += `
    <tr>
      <td>${item.title}</td>
      <td width="120">
        ${item.origin_price}
      </td>
      <td width="120">
        ${item.price}
      </td>
      <td width="100">
        <div class="form-check form-switch">
          <input class="form-check-input" type="checkbox" id="is_enabled" ${item.is_enabled? 'checked': ''} data-action="complete" data-id="${item.id}">
          <label class="form-check-label" for="is_enabled">${item.is_enabled? '啟用' : '未啟用'}</label>
        </div>
      </td>
      <td width="120">
        <button type="button" class="btn btn-sm btn-danger move" data-action="remove" data-id="${item.id}"> 刪除 </button>
      </td>
    </tr>`;
  })
  document.getElementById('productList').innerHTML = str;
  document.getElementById('productCount').textContent = productData.length;
});

let str = '';
productData.forEach((item) => {
  str += `
  <tr>
    <td>${item.title}</td>
    <td width="120">
      ${item.origin_price}
    </td>
    <td width="120">
      ${item.price}
    </td>
    <td width="100">
      <div class="form-check form-switch">
        <input class="form-check-input" type="checkbox" id="is_enabled" ${item.is_enabled? 'checked': ''} data-action="complete" data-id="${item.id}">
        <label class="form-check-label" for="is_enabled">${item.is_enabled? '啟用' : '未啟用'}</label>
      </div>
    </td>
    <td width="120">
      <button type="button" class="btn btn-sm btn-danger move" data-action="remove" data-id="${item.id}"> 刪除 </button>
    </td>
  </tr>`;
})
document.getElementById('productList').innerHTML = str;
document.getElementById('productCount').textContent = productData.length;

function renderPage(data) {
  
}

關注點分離:


let productData = []

let addBtn = document.getElementById('addProduct');
let clearall = document.getElementById('clearAll');
let clearindex = document.getElementById('productList');

let productTitle = document.getElementById('title');
let productPrice = document.getElementById('origin_price');
let Price = document.getElementById('price');
let Count = document.getElementById('productCount');



//資料處理
function addProduct(){
 const timeStamp = Math.floor(Date.now());
 if (document.getElementById('title').value.trim() !== '') {
   productData.push({
     id: timeStamp,
     title: document.getElementById('title').value.trim(),
     origin_price: parseInt(document.getElementById('origin_price').value) || 0,
     price: parseInt(document.getElementById('price').value) || 0,
     is_enabled: false,
   });
   
   renderPage(productData);
   //空字串清空資料
   productTitle.value = '';
   productPrice.value = '';
   Price.value = '';
 }
};
addBtn.addEventListener('click', addProduct);


// 資料全部刪除
function clearAll(e){
 e.preventDefault();
 productData = [];
 renderPage(productData);
};
clearall.addEventListener('click', clearAll);


function productList(e){
 const action = e.target.dataset.action;
 const id = e.target.dataset.id;
 if (action === 'remove') {
   let newIndex = 0;
   productData.forEach((item, key) => {
     if (id == item.id) {
       newIndex = key;
     }
   })
   productData.splice(newIndex, 1);

 } else if (action === 'complete') {
   productData.forEach((item) => {
     if (id == item.id) {
       item.is_enabled = !item.is_enabled;
     }
   })
 }

 renderPage(productData);
}
clearindex.addEventListener('click' ,productList)



// 渲染畫面
function renderPage(data){
 let str = '';
 productData.forEach((item) => {
   str += `
   <tr>
     <td>${item.title}</td>
     <td width="120">
       ${item.origin_price}
     </td>
     <td width="120">
       ${item.price}
     </td>
     <td width="100">
       <div class="form-check form-switch">
         <input class="form-check-input" type="checkbox" id="is_enabled" ${item.is_enabled? 'checked': ''} data-action="complete" data-id="${item.id}">
         <label class="form-check-label" for="is_enabled">${item.is_enabled? '啟用' : '未啟用'}</label>
       </div>
     </td>
     <td width="120">
       <button type="button" class="btn btn-sm btn-danger move" data-action="remove" data-id="${item.id}"> 刪除 </button>
     </td>
   </tr>`;
 })
 clearindex.innerHTML = str;
 Count.textContent = data.length;
}

renderPage(productData);

關注點分離的版本,相當簡潔,整體下來約縮減約五十幾行的程式碼。 /images/emoticon/emoticon07.gif


參考資料:
HiSKIO 程式語言線上教學
Kuro
Microsoft MVC
架構原則
ALPHA Camp


上一篇
Vue.js 從零開始:SSR、MPA、SPA的概念
下一篇
Vue.js 從零開始:This 是什麼?
系列文
Vue.js 從零開始30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言