隨著React Vue前端框架的興起,出現(xiàn)了Vue-router,react-router-dom等前端路由管理庫,利用他們構(gòu)建出來的單頁面應(yīng)用,也是越來越接近原生的體驗(yàn),再也不是以前的點(diǎn)擊標(biāo)簽跳轉(zhuǎn)頁面,刷新整個(gè)頁面了,那么他們的原理是什么呢?優(yōu)質(zhì)gitHub開源練手項(xiàng)目:
React移動端從零開始手寫一個(gè)優(yōu)化版腳手架
Electron跨平臺應(yīng)用DEMO
手寫的Node.js靜態(tài)資源服務(wù)器
React 同構(gòu)ssr
Node.js爬蟲輸出PDF文件
先說說原始的MPA多頁面應(yīng)用:文末還有新建的QQ以及微信群哦~ 歡迎大家加入~~傳統(tǒng)的多頁面應(yīng)用構(gòu)建方式:
純服務(wù)端渲染,前后端不分離,使用jsp,jade,"ejs","tempalte"等技術(shù)在后臺先拼接成對應(yīng)的HTML結(jié)構(gòu),然后轉(zhuǎn)換成字符串,在每個(gè)對應(yīng)的路由返回對應(yīng)的數(shù)據(jù)(文件)即可
Jade模版服務(wù)端渲染,代碼實(shí)現(xiàn):
const express= require("express") const app =express() const jade = require("jade") const result = *** const url path = *** const html = jade.renderFile(url, { data: result, urlPath })//傳入數(shù)據(jù)給模板引擎 app.get("/",(req,res)=>{ res.send(html)//直接吐渲染好的`html`文件拼接成字符串返回給客戶端 }) //RestFul接口 app.listen(3000,err=>{ //do something })
使用jQuery等傳統(tǒng)庫繪制的前端頁面
傳統(tǒng)前后端不分離,服務(wù)端渲染的優(yōu)缺點(diǎn): 優(yōu)點(diǎn):SEO友好,因?yàn)榉祷亟o前端的是渲染好的HTML結(jié)構(gòu),里面的內(nèi)容都可以被爬蟲抓取到。
對于一些應(yīng)用性能等要求不高的項(xiàng)目,比如某個(gè)公司的靜態(tài)網(wǎng)頁,內(nèi)容很少的情況下,直接一把梭就好,不用再搭建工程化的環(huán)境等
對于后端程序員(全干工程師)來說,不用去特意學(xué)習(xí)前端框架,公司也不用特意去招聘前端
兼容性好,傳統(tǒng)服務(wù)端渲染多頁面應(yīng)用吐出來的都是字符串,HTML結(jié)構(gòu)
缺點(diǎn):如果項(xiàng)目很大,不利于維護(hù),據(jù)我所知,目前很多云計(jì)算公司,還有不少都是使用非單頁面應(yīng)用,例如一個(gè)幾十萬行的項(xiàng)目是用jQuery寫的,如果注釋和文檔不是非常齊全,那么真的會無從下手
性能和用戶體驗(yàn),不能跟單頁面應(yīng)用相比
后期迭代,升級空間不大,目前大部分寫得比較好的庫,都建立vue,react等框架基礎(chǔ)上,他們都有一套自己的運(yùn)行機(jī)制,有自己的生命周期,并且不像傳統(tǒng)的應(yīng)用,還加上了一層虛擬DOM以及diff算法
現(xiàn)在類似Ant-Design-pro這樣的開箱即用的庫已經(jīng)很多,單頁面應(yīng)用的學(xué)習(xí)和開發(fā)成本已經(jīng)很低很低,如果還在使用傳統(tǒng)的技術(shù)去開發(fā)新的應(yīng)用,對于開發(fā)人員多內(nèi)心來說也是一種折磨。
這里并不是說多頁面應(yīng)用不好,只能說各有各自的好,單頁面應(yīng)用如果通過大量的極致優(yōu)化手段,是可以從不少方面跟原生一拼。目前的單頁面應(yīng)用:
只有一張Web頁面的應(yīng)用,是一種從Web服務(wù)器加載的富客戶端,單頁面跳轉(zhuǎn)僅刷新局部資源 ,公共資源(js、css等)僅需加載一次,常用于PC端官網(wǎng)、購物等網(wǎng)站
其實(shí)只有一個(gè)空的DIV標(biāo)簽,其他都是js動態(tài)生態(tài)的內(nèi)容
單頁面應(yīng)用實(shí)現(xiàn)步驟: 代碼實(shí)現(xiàn):首先是一個(gè)靜態(tài)模板文件 index.html
Document
在vue react框架的入口文件中指定對應(yīng)的渲染元素:
import React from "react; import ReactDOM from "react-dom"; ReactDOM.render(, document.querySelector("#root") )
引入react-router或者 react-router-dom,dva等路由跳轉(zhuǎn)的庫
配置路由跳轉(zhuǎn)
單頁面應(yīng)用所謂路由跳轉(zhuǎn),其實(shí)最終結(jié)果就是://這里使用HashRouter //React錯(cuò)誤邊界 //404路由或者重定向都可以
瀏覽器的url地址發(fā)生變化,但是其實(shí)并沒有發(fā)送請求,也沒有刷新整個(gè)頁面
根據(jù)我們配置的路由信息,每次點(diǎn)擊切換路由,會切換到不同的組件顯示,類似于選項(xiàng)卡功能的實(shí)現(xiàn),但是同時(shí)url地址欄會變化
分為HashRouter和BrowserRouter兩種模式
自己實(shí)現(xiàn)一個(gè)粗略的路由跳轉(zhuǎn): 自己實(shí)現(xiàn)傳統(tǒng)的Hash模式跳轉(zhuǎn):hash 就是指 url 后的 # 號以及后面的字符。例如www.baidu.com/#segmentfault,那么#segmentfault就是hash值
需要用到的幾個(gè)知識點(diǎn):
window.location.hash = "**"; // 設(shè)置當(dāng)前的hash值
const hash = window.location.hash 獲取當(dāng)前的hash值
hash改變會觸發(fā)window的hashchange事件
window.onhashchange=function(e){ let newURL = e.newURL; // 改變后的新 url地址 let oldURL = e.oldURL; // 改變前的舊 url地址 }
這里特別注意,hash改變并不會發(fā)送請求開始實(shí)現(xiàn)Hash模式跳轉(zhuǎn): 使用類似發(fā)布訂閱模式的方式,使用ES6的class實(shí)現(xiàn):
初始訂閱,每個(gè)不同的hash值,對應(yīng)不同的函數(shù)調(diào)用處理。
class Router { constructor() { this.routes = {}; this.currentUrl = ""; } route(path, callback) { this.routes[path] = callback || function() {}; } updateView() { this.currentUrl = location.hash.slice(1) || "/"; this.routes[this.currentUrl] && this.routes[this.currentUrl](); } init() { window.addEventListener("load", this.updateView.bind(this), false); window.addEventListener("hashchange", this.updateView.bind(this), false); } }
routes 用來存放不同路由對應(yīng)的回調(diào)函數(shù)
init 用來初始化路由,在 load 事件發(fā)生后刷新頁面,并且綁定 hashchange 事件,當(dāng) hash 值改變時(shí)觸發(fā)對應(yīng)回調(diào)函數(shù)
開始使用:這樣一個(gè)簡單的hash模式路由就做好了,剩下的就是路由嵌套,以及錯(cuò)誤邊界的處理History模式實(shí)現(xiàn):
History來自Html5的規(guī)范
History模式,url地址欄的改變并不會觸發(fā)任何事件
History模式,可以使用history.pushState,history.replaceState來控制url地址,history.pushState() 和 history.replaceState() 的區(qū)別在于:
history.pushState() 在保留現(xiàn)有歷史記錄的同時(shí),將 url 加入到歷史記錄中。
history.replaceState() 會將歷史記錄中的當(dāng)前頁面歷史替換為 url。
History模式下,刷新頁面會404,需要后端配合匹配一個(gè)任意路由,重定向到首頁,特別是加上Nginx反向代理服務(wù)器的時(shí)候
我們需要換個(gè)思路,我們可以羅列出所有可能觸發(fā) history 改變的情況,并且將這些方式一一進(jìn)行攔截,變相地監(jiān)聽 history 的改變。對于一個(gè)應(yīng)用而言,url 的改變(不包括 hash 值得改變)只能由下面三種情況引起:
點(diǎn)擊瀏覽器的前進(jìn)或后退按鈕
點(diǎn)擊 a 標(biāo)簽
在 JS 代碼中觸發(fā) history.push(replace)State 函數(shù)
只要對上述三種情況進(jìn)行攔截,就可以變相監(jiān)聽到 history 的改變而做出調(diào)整。針對情況 1,HTML5 規(guī)范中有相應(yīng)的 onpopstate 事件,通過它可以監(jiān)聽到前進(jìn)或者后退按鈕的點(diǎn)擊,值得注意的是,調(diào)用 history.push(replace)State 并不會觸發(fā) onpopstate 事件。開始實(shí)現(xiàn):
class Router { constructor() { this.routes = {}; this.currentUrl = ""; } route(path, callback) { this.routes[path] = callback || function() {}; } updateView(url) { this.currentUrl = url; this.routes[this.currentUrl] && this.routes[this.currentUrl](); } bindLink() { const allLink = document.querySelectorAll("a[data-href]"); for (let i = 0, len = allLink.length; i < len; i++) { const current = allLink[i]; current.addEventListener( "click", e => { e.preventDefault(); const url = current.getAttribute("data-href"); history.pushState({}, null, url); this.updateView(url); }, false ); } } init() { this.bindLink(); window.addEventListener("popstate", e => { this.updateView(window.location.pathname); }); window.addEventListener("load", () => this.updateView("/"), false); } }Router 跟之前 Hash 路由很像,不同的地方在于:
init 初始化函數(shù),首先需要獲取所有特殊的鏈接標(biāo)簽,然后監(jiān)聽點(diǎn)擊事件,并阻止其默認(rèn)事件,觸發(fā) history.pushState 以及更新相應(yīng)的視圖。
另外綁定 popstate 事件,當(dāng)用戶點(diǎn)擊前進(jìn)或者后退的按鈕時(shí)候,能夠及時(shí)更新視圖,另外當(dāng)剛進(jìn)去頁面時(shí)也要觸發(fā)一次視圖更新。
實(shí)際使用:跟之前的 html 基本一致,區(qū)別在于用 data-href 來表示要實(shí)現(xiàn)軟路由的鏈接標(biāo)簽。
當(dāng)然上面還有情況 3,就是你在 JS 直接觸發(fā) pushState 函數(shù),那么這時(shí)候你必須要調(diào)用視圖更新函數(shù),否則就是出現(xiàn)視圖內(nèi)容和 url 不一致的情況。
setTimeout(() => { history.pushState({}, null, "/about"); router.updateView("/about"); }, 2000);React-router-dom源碼: Router組件:
export class Route extends Component { componentWillMount() { window.addEventListener("hashchange", this.updateView, false); } componentWillUnmount() { window.removeEventListener("hashchange", this.updateView, false); } updateView = () => { this.forceUpdate(); } render() { const { path, exact, component } = this.props; const match = matchPath(window.location.hash, { exact, path }); if (!match) { return null; } if (component) { return React.createElement(component, { match }); } return null; } }
組件掛載監(jiān)聽hash change原生事件,將要卸載時(shí)候移除事件監(jiān)聽防止內(nèi)存泄漏
每次hash改變,就觸發(fā)所有對應(yīng)hash的回掉,所有的Router都去更新視圖
每個(gè)Router組件中,都去對比當(dāng)前的hash值和這個(gè)組件的path屬性,如果不一樣,那么就返回null,·否則就渲染這個(gè)組件對應(yīng)的視圖
History模式的實(shí)現(xiàn):實(shí)現(xiàn)History
這里想多留些時(shí)間寫其他源碼,這篇文章寫得非常好,大家也可以去看看,本文很多借鑒他的。withRouter高階函數(shù)的源碼:
var withRouter = function withRouter(Component) { var C = function C(props) { var wrappedComponentRef = props.wrappedComponentRef, remainingProps = _objectWithoutProperties(props, ["wrappedComponentRef"]); return _react2.default.createElement(_Route2.default, { children: function children(routeComponentProps) { return _react2.default.createElement(Component, _extends({}, remainingProps, routeComponentProps, { ref: wrappedComponentRef })); } }); }; C.displayName = "withRouter(" + (Component.displayName || Component.name) + ")"; C.WrappedComponent = Component; C.propTypes = { wrappedComponentRef: _propTypes2.default.func }; return (0, _hoistNonReactStatics2.default)(C, Component); };
傳入一個(gè)組件,返回一個(gè)新的組件,并且給這個(gè)組件賦予全局屬性,擁有路由組件的三大屬性
Switch組件:Switch.prototype.render = function render() { var route = this.context.router.route; var children = this.props.children; var location = this.props.location || route.location; var match = void 0, child = void 0; _react2.default.Children.forEach(children, function (element) { if (match == null && _react2.default.isValidElement(element)) { var _element$props = element.props, pathProp = _element$props.path, exact = _element$props.exact, strict = _element$props.strict, sensitive = _element$props.sensitive, from = _element$props.from; var path = pathProp || from; child = element; match = (0, _matchPath2.default)(location.pathname, { path: path, exact: exact, strict: strict, sensitive: sensitive }, route.match); } }); return match ? _react2.default.cloneElement(child, { location: location, computedMatch: match }) : null; };
遍歷所以傳入的子元素
如果有符合的路由對應(yīng)的元素,那么就返回,而且只匹配這一個(gè)路由。不再繼續(xù)往下匹配
如果第二條沒有找到符合的元素,那么拋出錯(cuò)誤
如果覺得寫得好,記得點(diǎn)個(gè)贊哦,另外新建了微信和QQ群,歡迎各位小哥哥小姐姐入駐~
微信群:
QQ群
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/106267.html
showImg(https://segmentfault.com/img/bVbvOmp?w=1612&h=888); 隨著React Vue前端框架的興起,出現(xiàn)了Vue-router,react-router-dom等前端路由管理庫,利用他們構(gòu)建出來的單頁面應(yīng)用,也是越來越接近原生的體驗(yàn),再也不是以前的點(diǎn)擊標(biāo)簽跳轉(zhuǎn)頁面,刷新整個(gè)頁面了,那么他們的原理是什么呢? 優(yōu)質(zhì)gitHub開源練手項(xiàng)目: ...
showImg(https://segmentfault.com/img/bVbvOmp?w=1612&h=888); 隨著React Vue前端框架的興起,出現(xiàn)了Vue-router,react-router-dom等前端路由管理庫,利用他們構(gòu)建出來的單頁面應(yīng)用,也是越來越接近原生的體驗(yàn),再也不是以前的點(diǎn)擊標(biāo)簽跳轉(zhuǎn)頁面,刷新整個(gè)頁面了,那么他們的原理是什么呢? 優(yōu)質(zhì)gitHub開源練手項(xiàng)目: ...
摘要:談起閉包,它可是兩個(gè)核心技術(shù)之一異步基于打造前端持續(xù)集成開發(fā)環(huán)境本文將以一個(gè)標(biāo)準(zhǔn)的項(xiàng)目為例,完全拋棄傳統(tǒng)的前端項(xiàng)目開發(fā)部署方式,基于容器技術(shù)打造一個(gè)精簡的前端持續(xù)集成的開發(fā)環(huán)境。 這一次,徹底弄懂 JavaScript 執(zhí)行機(jī)制 本文的目的就是要保證你徹底弄懂javascript的執(zhí)行機(jī)制,如果讀完本文還不懂,可以揍我。 不論你是javascript新手還是老鳥,不論是面試求職,還是日...
摘要:一,什么是單頁面應(yīng)用通俗的來講,就是一個(gè)應(yīng)用只有一個(gè)頁面,用戶通過切換路由和動態(tài)獲取數(shù)據(jù)達(dá)到頁面更新的目的,整個(gè)應(yīng)用的使用過程中,頁面只是局部刷新。 一, 什么是單頁面應(yīng)用 通俗的來講,就是一個(gè)應(yīng)用只有一個(gè)頁面,用戶通過切換路由和動態(tài)獲取數(shù)據(jù)達(dá)到頁面更新的目的,整個(gè)應(yīng)用的使用過程中,頁面只是局部刷新。在整個(gè)應(yīng)用初始加載時(shí),會一次性加載所有靜態(tài)文件或所有公共靜態(tài)文件(切換頁面時(shí),加載相應(yīng)...
閱讀 3464·2021-11-18 10:02
閱讀 3706·2021-09-13 10:25
閱讀 1919·2021-07-26 23:38
閱讀 2568·2019-08-30 15:44
閱讀 2266·2019-08-30 13:51
閱讀 1222·2019-08-26 11:35
閱讀 2273·2019-08-26 10:29
閱讀 3444·2019-08-23 14:56