Router入門0x201: 從 URL 到 SPA

0x000 概述

從這一章開始就進入路由章節了,並不直接從如何使用react-route來說,而是從路由的概念和實現來說,達到知道路由的本質,而不是隻知道如何使用react-route庫的目的,畢竟react-route只是一個庫,是路由的一個實現而已,而不是路由自己。css

0x001 URL的概念

不少人對url的理解就是網址,咱們在瀏覽器地址欄輸入網址,即可以訪問到特定網頁,但其實url的含義遠遠不止是網址。url的全稱是統一資源定位符(英文:Uniform Resource Locator),能夠這麼說,url是一種標準,而網址則是符合url標準的一種實現而已。html

讓咱們作幾個實驗:vue

  1. 打開瀏覽器,訪問segmentfault的主頁,此時地址欄顯示的是:react

    https://segmentfault.com
  2. 桌面新建from-url-to-spa.txt文件,輸入內容from url to spa,並拖拽到瀏覽器,此時瀏覽器顯示的是webpack

    file:///Users/FollowWinter/Desktop/from-url-to-spa.txt
  3. 打開一個github項目,並選擇ssh訪問,咱們能夠獲得如下地址:git

    git@github.com:followWinter/flex-layout.git

說明:其中,1訪問了一個網頁,2訪問了一個本地文件,3訪問了一個開源項目,從以上能夠看出,url有多種用途各異的實現,可是咱們能夠這麼概括,網絡上(包括本地和遠程)全部的的東西都看做資源,咱們能夠經過一種符合某種標準的格式來訪問這種資源,從而忽略設備類型(服務器、路由器、硬盤......)、網絡類型(遠程、本地......)、資源類型(文本、圖片、音樂、電影......),而這種標準就是url,也就是我對統一資源定位符的理解。es6

  • 統一資源定位符的標準格式以下:github

    協議類型:[//服務器地址[:端口號]][/資源層級UNIX文件路徑]文件名[?查詢][#片斷ID]
  • 統一資源定位符的完整格式以下:web

    協議類型:[//[訪問資源須要的憑證信息@]服務器地址[:端口號]][/資源層級UNIX文件路徑]文件名[?查詢][#片斷ID]

0x002 spa是什麼

SPA全稱是single page web application,也就是隻有一個頁面的web應用程序,咱們訪問一個網頁,可以在這個網頁上完成全部的業務操做,咱們就能夠稱之爲SPA,是和框架無關、技術無關的一個概念。並非說用angularvuereact實現的web應用才叫SPA,由於這些框架也能夠在多頁應用中使用。json

0x003 如何實現spa

只要在一個頁面完成全部業務操做,就能夠稱之爲SPA了,因此實現所謂的SPA也很簡單,就是將本來多頁的步驟轉化爲一個頁面就好了。

0x004 SPA和路由有啥關係啊

回答:沒有關係。SPA不必定要使用路由,不使用也沒有關係,可是隨着單頁應用了擴大,將全部的邏輯都卸載一個頁面上,會致使邏輯爆炸,維護痛苦,因此在邏輯上又分爲多個頁面,達到好維護的效果。

0x005 路由出現

一開始是沒有路由的,可是作的應用多了,便有了路由。對於路由的需求有兩個:

  1. 維護上的需求,過多的邏輯寫在一個頁面上,容易混亂,因此用路由分離單獨邏輯和頁面。
  2. 狀態保存的需求,好比一個SPA,咱們有文章和文章詳情頁,有一天咱們須要分享一個文章,但願能夠經過一個連接直接訪問到這篇文章。可是單頁應用是無狀態的,而網址又是惟一的,好比a.com/index.html,沒法作到直接訪問詳情頁,因此就出現了一些方案:

    • hash:a.com/index.html#detail/1,訪問 id 爲1的文章詳情頁
    • url:a.com/index/detail/1,訪問 id 爲1的文章詳情頁

這樣咱們就能夠分享一篇文章給其餘用戶了,方案1實現比較簡單,可是路由醜陋而且佔用了 hash 符,頁面中就不能亂用 hash 符了。方案2好可是須要後端配合,實現也很簡單,無論這個 url 是什麼,都返回單頁應用的 html 就行了。

0x006 實現簡單的SPA

  1. 架構:

    • 組件,每一個組件都是獨立的,能夠渲染出本身的dom,而且能夠綁定事件,擁有生命週期。
    • 渲染器,將組件渲染到頁面上。
    • 服務,作數據管理等一些邏輯服務。
  2. 項目初始化:

    整個項目起始沒有啥特別的,只是支持了 es6而已,而整個項目咱們也將會用 es6來實現
    • 初始化項目及其目錄

      + 0x021-spa
          + src 
              + core
              + page
              + services
              - index.html
              - index.js
          - .babelrc
          - package.json
          - webpack.config.js
    • index.html:

      <!doctype html>
      <html>
      <head>
          <title>React Study</title>
          <!--直接引入`bootstrap`樣式,讓 `demo` 好看一點-->
          <link href="https://cdn.bootcss.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet">
      </head>
      <body class="container">
      <div id="app">
      </div>
      </body>
      </html>
    • .babelrc

      {
        "presets": [
          "env",
          "stage-3"
      
        ]
      }
    • package.json

      {
        "name": "0x021-spa",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
          "test": "echo \"Error: no test specified\" && exit 1",
          "start": "webpack-dev-server --color --process "
        },
        "keywords": [],
        "author": "",
        "license": "ISC",
        "devDependencies": {
          "babel-cli": "^6.26.0",
          "babel-core": "^6.26.3",
          "babel-loader": "^7.1.5",
          "babel-preset-env": "^1.7.0",
          "babel-preset-react": "^6.24.1",
          "babel-preset-stage-3": "^6.24.1",
          "html-webpack-plugin": "^3.2.0",
          "webpack": "^4.16.5",
          "webpack-cli": "^3.1.0",
          "webpack-dev-server": "^3.1.5"
        }
       
      }
      • webpack.config.js:

        const path = require('path')
           var HtmlWebpackPlugin = require('html-webpack-plugin');
           
           module.exports = {
           entry: path.resolve(__dirname, 'src/index.js'),
           mode: 'development',
           output: {
               path: path.resolve(__dirname, 'dist'),
               filename: 'bundle.js'
           },
           devServer: {
               open: true
           },
           module: {
               rules: [
                   {
                       test: /\.js$/,
                       loader: "babel-loader"
                   },
           
               ]
           },
           plugins: [
               new HtmlWebpackPlugin({
                   template: path.resolve(__dirname, "src/index.html")
               })
           ]
           }
  3. 渲染器實現

    渲染器的做用起始就是渲染組件而已,而每一個組件都有一個 render方法,該方法返回一個 dom字符串,也就是說,渲染器的本質就是將 dom字符串掛載和卸載。
    • core/LeactDom.js

      class LeactDom {
          static render(child, parent) {
              parent.innerHTML=child
          }
      }
      
      export default LeactDom
    • 測試index.js

      import LeactDom from "./core/LeactDom";
      import LeactDom from "./core/LeactDom";
      
      LeactDom.render(`<p id="p">這是一個p</p>`, document.getElementById('app'))
      document.getElementById('p').addEventListener('click', () => {
          LeactDom.render("<span>這是一個span</span>", document.getElementById('app'))
      
      })
    • 查看瀏覽器

      如圖,咱們已經實現了切換了,只須要將之封裝爲組件就好了
      ![圖片描述][1]
  4. 組件

    • core/Component.js

      // 這是組件根類, 全部的組件都繼承這個根
      class Component {
          // 返回 dom 字符串
         render() {
              return ''
          }
      
          // dom 掛載上去之後 執行該方法, 能夠在這個方法上執行 dom 查詢和事件綁定
          componentDidMount() {
      
          }
      
      }
      
      export default Component
    • 自定義組件page/Hello.js

      import Component from "../core/Component";
      
      class Hello extends Component {
      
          render() {
              return `<p id='hello'>hello</p>`
          }
      
          componentDidMount() {
              document.getElementById('hello').addEventListener('click', () => {
                  alert('hello')
              })
          }
      }
      
      export default Hello
    • 引入Hello組件

      import LeactDom from "./core/LeactDom";
      import Hello from "./page/Hello";
      
      LeactDom.render(Hello,document.getElementById('app'))
    • 修改LeactDom

      class LeactDom {
      
          static render(child, parent, props={}) {
              if (typeof child === 'function') {
                  let comp = new child()
                  comp.props = props
                  parent.innerHTML = comp.render()
                  comp.componentDidMount()
              } else {
                  parent.innerHTML = child
              }
          }
      }
      
      export default LeactDom
    • 查看效果

      clipboard.png

  5. 框架完成開始編寫服務

    • 文章獲取服務service/AticleService.js

      const articles = [
          {
              id: 1,
              title: "Redux入門0x101: 簡介及`redux`簡單實現",
              summary: "簡介及`redux`簡單實現",
              detail: "詳情1"
          },
          {
              id: 2,
              title: "Redux入門0x102: redux 栗子之 counter",
              summary: "redux 栗子之 counter",
              detail: "詳情2"
          },
          {
              id: 3,
              title: "Redux入門0x103: 拆分多個 reducer",
              summary: "拆分多個 reducer",
              detail: "詳情3"
          },
          {
              id: 4,
              title: "Redux入門0x104: Action Creators",
              summary: "Action Creators",
              detail: "詳情4"
          },
          {
              id: 5,
              title: "Redux入門0x105: redux 中間件",
              summary: "redux 中間件",
              detail: "詳情5"
          },
      
      ]
      
      class ArticleService {
      
          static getAll() {
              return articles
          }
      
          static getById(id) {
              return articles.find((article) => {
                  return id == article.id
              })
          }
      }
      
      export default ArticleService
  6. 開始編寫自定義組件

    • 文章列表組件

      import ArticleService from "../services/ArticleService";
      import DetailPage from "./DetailPage";
      import LeactDom from "../core/LeactDom";
      
      class ArticlePage {
          render() {
              let articlesListString = ArticleService.getAll()
                  .map(article => {
                      return `<div class="article" data-id="${article.id}">
                      <h5>${article.title}</h5>
                      <p>${article.summary}</p>
                      <hr>
                  </div>`
                  })
                  .reduce((article1, article2) => {
                      return article1 + article2
                  })
              let articleListContrinerString = `<div>
                  <h3>文章列表</h3>
                  <hr>
                  <div>
                  ${articlesListString}
                  </div>
              </div>`
      
              return articleListContrinerString
          }
      
          componentDidMount() {
              let articles = document.getElementsByClassName('article')
              ;[].forEach.call(articles, article => {
                      article.addEventListener('click', () => {
                          LeactDom.render(new DetailPage({articleId: article.getAttribute('data-id')}), document.getElementById('app'))
                      })
                  }
              )
      
          }
      
      }
      
      export default ArticlePage
    • 文章詳情組件

      import ArticleService from "../services/ArticleService";
      import Component from "../core/Component";
      import LeactDom from "../core/LeactDom";
      import ArticlePage from "./ArticlePage";
      
      class DetailPage extends Component {
          constructor(props) {
              super()
              this.article = ArticleService.getById(props.articleId)
          }
      
          render() {
              const {title, summary, detail} = this.article
              return `<div>
                  <h3>${title}</h3>
                  <p>${summary}</p>
                  <hr>
                  <p>${detail}</p>
                  <button id="back" type="button" class="btn btn-success">返回</button>
              </div>`
          }
      
      
          componentDidMount() {
              document.getElementById('back').addEventListener('click', () => {
                  LeactDom.render(new ArticlePage(), document.getElementById('app'))
              })
          }
      }
      
      export default DetailPage
  7. 加載組件index.js

    import LeactDom from "./core/LeactDom";
    import ArticlePage from "./page/ArticlePage";
    
    LeactDom.render(new ArticlePage(),document.getElementById('app'))

8 查看最終效果

圖片描述

0x007 總結

這裏要作的只是一個案例,而不是寫一個完整的框架,因此在不少地方並無完善,只是爲了驗證明現SPA的方式,而結果也確實驗證了。也將一些問題暴露出來了,其餘的問題咱們不關心,咱們只關心咱們以前提出的問題,只有一個網址,如何將某個頁面分享出去,很明顯,作成SPA以後,沒法將文章詳情頁面分享給他人。解決 方法也已經給出來了:

  • hash
  • url

將在下一張講述如何解決

0x008 資源

相關文章
相關標籤/搜索