從這一章開始就進入路由章節了,並不直接從如何使用react-route
來說,而是從路由的概念和實現來說,達到知道路由的本質,而不是隻知道如何使用react-route
庫的目的,畢竟react-route
只是一個庫,是路由的一個實現而已,而不是路由自己。css
不少人對url
的理解就是網址,咱們在瀏覽器地址欄輸入網址,即可以訪問到特定網頁,但其實url
的含義遠遠不止是網址。url
的全稱是統一資源定位符(英文:Uniform Resource Locator),能夠這麼說,url
是一種標準,而網址則是符合url
標準的一種實現而已。html
讓咱們作幾個實驗:vue
打開瀏覽器,訪問segmentfault
的主頁,此時地址欄顯示的是:react
https://segmentfault.com
桌面新建from-url-to-spa.txt
文件,輸入內容from url to spa
,並拖拽到瀏覽器,此時瀏覽器顯示的是webpack
file:///Users/FollowWinter/Desktop/from-url-to-spa.txt
打開一個github項目,並選擇ssh訪問,咱們能夠獲得如下地址:git
git@github.com:followWinter/flex-layout.git
說明:其中,1訪問了一個網頁,2訪問了一個本地文件,3訪問了一個開源項目,從以上能夠看出,url有多種用途各異的實現,可是咱們能夠這麼概括,網絡上(包括本地和遠程)全部的的東西都看做資源,咱們能夠經過一種符合某種標準的格式來訪問這種資源,從而忽略設備類型(服務器、路由器、硬盤......)、網絡類型(遠程、本地......)、資源類型(文本、圖片、音樂、電影......),而這種標準就是url,也就是我對統一資源定位符的理解。es6
統一資源定位符的標準格式以下:github
協議類型:[//服務器地址[:端口號]][/資源層級UNIX文件路徑]文件名[?查詢][#片斷ID]
統一資源定位符的完整格式以下:web
協議類型:[//[訪問資源須要的憑證信息@]服務器地址[:端口號]][/資源層級UNIX文件路徑]文件名[?查詢][#片斷ID]
SPA
全稱是single page web application
,也就是隻有一個頁面的web
應用程序,咱們訪問一個網頁,可以在這個網頁上完成全部的業務操做,咱們就能夠稱之爲SPA
,是和框架無關、技術無關的一個概念。並非說用angular
、vue
、react
實現的web
應用才叫SPA
,由於這些框架也能夠在多頁應用中使用。json
spa
只要在一個頁面完成全部業務操做,就能夠稱之爲SPA
了,因此實現所謂的SPA
也很簡單,就是將本來多頁的步驟轉化爲一個頁面就好了。
SPA
和路由有啥關係啊回答:沒有關係。SPA
不必定要使用路由,不使用也沒有關係,可是隨着單頁應用了擴大,將全部的邏輯都卸載一個頁面上,會致使邏輯爆炸,維護痛苦,因此在邏輯上又分爲多個頁面,達到好維護的效果。
一開始是沒有路由的,可是作的應用多了,便有了路由。對於路由的需求有兩個:
狀態保存的需求,好比一個SPA
,咱們有文章和文章詳情頁,有一天咱們須要分享一個文章,但願能夠經過一個連接直接訪問到這篇文章。可是單頁應用是無狀態的,而網址又是惟一的,好比a.com/index.html
,沒法作到直接訪問詳情頁,因此就出現了一些方案:
a.com/index.html#detail/1
,訪問 id 爲1的文章詳情頁a.com/index/detail/1
,訪問 id 爲1的文章詳情頁這樣咱們就能夠分享一篇文章給其餘用戶了,方案1實現比較簡單,可是路由醜陋而且佔用了 hash 符,頁面中就不能亂用 hash 符了。方案2好可是須要後端配合,實現也很簡單,無論這個 url 是什麼,都返回單頁應用的 html 就行了。
SPA
架構:
dom
,而且能夠綁定事件,擁有生命週期。項目初始化:
整個項目起始沒有啥特別的,只是支持了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") }) ] }
渲染器實現
渲染器的做用起始就是渲染組件而已,而每一個組件都有一個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]
組件
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
查看效果
框架完成開始編寫服務
文章獲取服務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
開始編寫自定義組件
文章列表組件
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
加載組件index.js
import LeactDom from "./core/LeactDom"; import ArticlePage from "./page/ArticlePage"; LeactDom.render(new ArticlePage(),document.getElementById('app'))
8 查看最終效果
這裏要作的只是一個案例,而不是寫一個完整的框架,因此在不少地方並無完善,只是爲了驗證明現SPA
的方式,而結果也確實驗證了。也將一些問題暴露出來了,其餘的問題咱們不關心,咱們只關心咱們以前提出的問題,只有一個網址,如何將某個頁面分享出去,很明顯,作成SPA
以後,沒法將文章詳情頁面分享給他人。解決 方法也已經給出來了:
將在下一張講述如何解決