在前端三大框架並存的今天,vue已是前端必須掌握的一部分。而對於不少入門者,或者轉行前端的小夥伴們,我的以爲vue是一個很是適合入門的框架的之一。筆者我的以爲,不管從api的易學的角度出發,仍是從原理層面解析,vue仍是比react的簡單一些。記得某個大神的面試分享:若是面試官沒有vue跟react方向的要求,儘可能往vue的方向扯,我的以爲是個很是優秀的意見哈哈哈。css
身處跳槽漲薪的年代,相信不少同行們都已經背了不少面經。(雖然心裏有點鄙視背題庫的人,面試神同樣,工做zhu同樣)。 長久的發展,仍是得紮紮實實的打好基礎。若是面試官再也不追問面經,反過來請你介紹vue,你想好怎麼介紹你的vue項目嗎?html
本文的重點,如何介紹如何搭建vue項目,介紹你的vue項目。前端
半年~三年經驗的vue開發者。 如未接觸過vue,建議從官方文檔:vuejs.org/ 學習搭建先。vue
該文章重點爲普及知識點,以及部分知識點的解析。html5
Vue.js是一套構建用戶界面的漸進式框架。 他最大的優點,也是單頁面最大的優點,數據驅動與組件化。node
首先咱們mini的源碼瞭解vue如何完成數據驅動。如圖:react
從圖咱們就能夠簡單的分析出什麼叫MVVM。jquery
MVVM, 實際上爲M + V + VM。vue的框架就是一個內置的VM狀態,而M就是咱們的MODLE, V便是咱們的視圖。而經過咱們的M,就能實現對V的控制,就是咱們所說的數據驅動(模型控制視圖)。ViewModel 的內容會實時展示在 View 層,前端開發者不再必低效又麻煩,還耗性能(由於沒有diff算法)地經過操縱 DOM 去更新視圖。這就是一個從根源上,MVVM框架比傳統MVC框架的優點。webpack
咱們進一步手寫Mini版來了解vue,從源碼瞭解什麼是數據劫持。ios
首先構造一個vue實例。寫過vue初始化的都知道,初始化時須要傳入data,以及綁定元素標記el。咱們把它儲存起來。
class wzVue {
constructor(options){
this.$options = options;
this.$data = options.data;
this.$el = options.el;
}
}
複製代碼
首先看一下Observer的實現,以vue2.0爲例,咱們都知道數據劫持是經過Object.defineProperty。它自帶監聽get,set方法,咱們能夠用他實現一個簡單的綁定。
obsever(this.$data);
function obsever(){
Object.defineProperty( obj, key, {
get(){
},
set( newValue ){
value = newValue;
}
})
}
複製代碼
這裏很簡單,若是仍是不明白怎麼雙向綁定,舉個簡單的栗子:
<input type="text" v-modle="key" id="key"/>
// script
var data = {
key: 5
key2: 8
}
obsever(data);
data.key=6;//
function obsever(obj){
Object.defineProperty( obj, key, {
get(){
},
set( newValue ){
document.getElementById('key').val(newValue);//寫死'key'先,下文會講解
}
})
}
//寫死'key'先,下文會講解
document.getElementById('key').addEventListener( 'click', false, function(e){
obj.key = e.target.value;
})
複製代碼
這樣實現了雙向綁定,若是對象obj.key賦值,就會觸發set方法,同步input的數據;若是頁面手動輸入值,則經過監聽觸發set,同步到對象obj的值。此時你可能有一個疑問,咱們在vue賦值的時候,是直接修改上下文data數據的,並非修改對對象的值, 也就是this.key=6。是的,vue源碼中,先對data對象的數據進行了一次本地的數據劫持。以下文的proxyData。這樣的:
this.key ----> data.key(觸發) --->實現數據劫持
observer( data ){//監聽data數據,雙向綁定
if( !data || typeof(data) !== 'object'){
return;
}
Object.keys( data ).forEach( key => {
this.observerData(key, data, data[key]);//監聽data對象
this.proxyData( key );
})
}
observerData( key, obj, value ) {
this.observer(key);
const dep = new Dep();
Object.defineProperty( obj, key, {
get(){
},
set( newValue ){ //通知變化
}
})
}
proxyData(key){
Object.defineProperty( this, key, {
get(){
return this.$data[key];
},
set( newValue ){
this.$data[key] = newValue;
}
})
}
複製代碼
兩點須要強調的地方:
1)遍歷data的屬性,vue的源碼是用了Object.keys。它能按順序遍歷出不一樣的屬性,可是不一樣的瀏覽器中可能執行順序不同。
2)由於Object.defineProperty只能監聽一層結構,因此,對於多層級的Object結構來說,須要遍歷去一層一層往下監聽。
那若是連續賦值的,例如this.key = 1; this.key2 = 2; 上邊的雙向綁定代碼是寫死了「key"。
這時候是否發生了兩次賦值?那麼咱們怎麼知道,它觸發的對象是哪一個呢?這時候,vue的設計是設計了dep的概念,來存放每一個監聽對象的值。
class Dep{
constructor(){
this.deps = [];
}
addDep(dep){
this.deps.push(dep);
}
notiyDep(){
this.deps.forEach(dep => {
dep.update();
})
}
}
複製代碼
這裏不難理解。addDep既是爲了有數據變化時,插入的「對象」,表示須要劫持。 notiyDep便是該對象,已經須要被更新,執行對應的update方法。
那麼插入的對象是什麼呢(數組的單體)?單體確定,須要包含一個「dom」對象,還有對應監聽的「data」對象,二者關係綁定,才能實現數據同步。這個「單體」,咱們稱呼它爲「watcher」。
class Watcher{
constructor( vm, key, initVal, cb ){
this.vm = vm;//保存vue對象實例
this.key = key;//保存綁定的key
this.cb = cb;//同步二者的回調函數
this.initVal = initVal;//初始化值
this.vm[this.key];//觸發對象的get方法
}
update(){
this.cb.call( this.vm, this.vm[this.key], this.initVal );
}
}
複製代碼
截至目前爲止,obsever仍是沒有跟Watcher關聯上。在講他們怎麼關聯上以前,咱們再看看vue的設計思惟,它是由Watcher添加訂閱者,再由Dep添加變化。那麼Watcher是怎麼來的?從圖中的關係,咱們能夠看出由頁面解析出來的。這就是咱們要講的 Compile。
Compile,首先有一個「初始化視圖」的動做。
class Compile{
constructor( el, vm ){
this.$el = document.querySelector(el);
this.$vm = vm;
if (this.$el) {
this.$fragment = this.getNodeChirdren( this.$el );
this.$el.appendChild(this.$fragment);
}
}
getNodeChirdren( el ){
const frag = document.createDocumentFragment();
let child;
while( (child = el.firstChild )){
frag.appendChild( child );
}
return frag;
}
}
複製代碼
這裏應該不難理解,拿到template對象的id,遍歷完以後,賦值顯示在咱們的el元素中。接下來咱們重點講Compile產生的Watcher。咱們在Compile的原型中添加this.compile( this.$fragment);方法。對剛纔拿到template的模版進行繼續,看他用到哪些屬性。
compile( el ){
const childNodes = el.childNodes;
Array.from(childNodes).forEach( node => {
if( node.nodeType == 1 ) {//1爲元素節點
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach( attr => {
const attrName = attr.name;//屬性名稱
const attrVal = attr.value;//屬性值
if( attrName == "v-modle"){
this.zDir_model( node, attrVal );
}
})
} else if( node.nodeType == 2 ){//2爲屬性節點
console.log("nodeType=====22");
} else if( node.nodeType == 3 ){//3爲文本節點
this.compileText( node );
}
// 遞歸子節點
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
})
}
複製代碼
若是你對childNodes,nodeType,nodeList仍是一臉懵逼,建議移步到: 關於DOM和BOM知識點彙總: juejin.im/post/5efef0…
從上邊的mini源碼能夠看出,compile遍歷el的全部子元素,若是是文本類型,咱們就進行文本解析compileText。若是是input須要雙向綁定,咱們就進行zDir_model解析。
compileText( node ){
if( typeof( node.textContent ) !== 'string' ) {
return "";
}
const reg = /({{(.*)}})/;
const reg2 = /[^/{/}]+/;
const key = String((node.textContent).match(reg)).match(reg2);//獲取監聽的key
const initVal = node.textContent;//記錄原文本第一次的數據
updateText( node, this.$vm[key], initVal );
}
updateText( node, value, initVal ){
var reg = /{{(.*)}}/ig;
var replaceStr = String( initVal.match(reg) );
var result = initVal.replace(replaceStr, value );
node.textContent = result;
new Watcher( this.$vm, key, initVal, function( value, initVal ){
updateText( node, value, initVal );
});
}
複製代碼
咱們再看看compileText的源碼,大概意思爲,獲取到文本例如「個人名字{{name}}」的key,即爲name。而後name進行初始化賦值updateText, updateText的初始化結束後,添加訂閱數據變化,綁定更新函數Watcher。
而Watcher,正是綁定dep跟compile的橋樑。咱們修改一下添加到dep跟Watcher的代碼:
observerData( key, obj, value ) {
this.observer(key);
const dep = new Dep();
Object.defineProperty( obj, key, {
get(){
Dep.target && dep.addDep(Dep.target);//添加的代碼+++++++++++++++++
return value;
},
set( newValue ){ //通知變化
if (newValue === value) {
return;
}
value = newValue;
//通知變化
dep.notiyDep();//添加的代碼+++++++++++++++++
}
})
}
class Watcher{
constructor( vm, key, initVal, cb ){
this.vm = vm;
this.key = key;
this.cb = cb;
this.initVal = initVal;
Dep.target = this;//添加的代碼+++++++++++++++++
this.vm[this.key];
Dep.target = null;
}
update(){
this.cb.call( this.vm, this.vm[this.key], this.initVal );
}
}
複製代碼
這樣的話,咱們在新增一個Watcher的過程當中,將此時的整個Watcher的this對象賦值到Dep.target中。這時候咱們再調用一下this.vm[this.key]。vm便是vue實例對象,因此,Watcher的this.vm[this.key],便是vue實例中的,this.key。而咱們的key已經經過Object.defineProperty監聽,此時就會進入到Object.defineProperty的get方法中, Dep.target 此時不爲空,因此dep.addDep(Dep.target),便是watcher添加訂閱者到dep中。
這時候若是數據發生變化,即調用set方法,而後dep.notiyDep,notiyDep就會通知,由文本解析的例如{{key}}的watcher從新更新一遍值,即完成了雙向綁定。
若是是v-modle的話,即在解析時,每一個對象多加一個監聽,而後主動調用set方法。
node.addEventListener("input", e => {
vm[value] = e.target.value;
});
複製代碼
這就是vue整個雙向綁定的大體流程,所謂的數據驅動。
而後他有一個很大的缺陷,這個缺陷是,他知道驅動對象,卻沒法對數組進行驅動 (實際上也行) 。這裏vue的做者用了另一種思惟去解決這個問題。他重寫了數組的原型,把數組的'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'的方法重寫了一遍。也就是當你數組使用了這7個方法時,vue重寫的方法,會幫你變化中放入dep中。
這 個 (實際上也行) 其實也頗有學問。上述說,vue2.0沒法對數組進行數據監聽,其實真實的的測試中,Object.defineProperty是能夠監聽到數組變化的。可是隻能在已有的長度中,不能對其加長的長度。那你這時候可能會有疑問,那咱們重寫array的push方法就夠了,爲何要重寫7個呢?好吧,我也曾經有這樣的疑惑。後續,曾在帖子上,看到過vue的筆者回復過,印象中是這麼說的:Object.defineProperty對數組的監聽,消耗性能大於效果。也就是說,原本Object.defineProperty,爲了提高效率而產生,如今用在數組上,反而下降了效率,那不如干脆拒絕使用他。
因而,又有了vue-cli3.0數據劫持的改造。
那麼vue3.0是怎麼實現數據劫持的呢?
3.0中雙向綁定已經再也不是使用Object.defineProperty。而是proxy。proxy的引入,更高的效率,一方面解決了數組方面的問題,咱們能夠簡單看一下mini源碼的改造:
proxyData( data ){//監聽data數據,雙向綁定
if( !data || typeof(data) !== 'object'){
return;
}
const _this = this;
const handler = {
set( target, key, value ) {
const rest = Reflect.set(target, key, value);
_this.$binding[key].map( item => {
item.update();
})
return rest;
}
}
this.$data = new Proxy( data, handler );
}
複製代碼
vue的mini源碼解析到此爲止,如還有不明白的地方可留言。 可須要源碼可進入github查看:github.com/zhuangweizh…
由上述mini源碼,咱們能夠知道vue的數據驅動。MVVM相比MVC模式, 沒有頻繁的操做dom值,在開發中無疑時更高效的靈活頁面的觸發。可以讓咱們專一與邏輯js的抒寫,而具體的頁面變化,交給VM區處理。
咱們都知道,js執行的效率高於dom渲染的效率。若是咱們能提早經過js算出不一致的地方,再最後去「渲染」最終的差別。明顯的增長效益。
咱們列出diff算法的三步曲:
mixins 選項接收一個混入對象的數組。而vue正是利用他來擴展vue的實例。
咱們的全局方法等,均可以利用mixins快速的套入vue實例。
十一個生命週期,create, mount, update, activated, destroyed。分別先後。最後還有v2.5.0版本的errorCaptured。 完善的生命週期的更適合,程序順序的正確執行。
props, emit, slot,provide/inject,attrs/listeners,EventBus emit/on,parent / children與 ref
也是你爲何選擇vue的緣由
筆者曾是一名jq的前端小雜,入門這些玩意,我的以爲他們的難度級別(僅限於api):
jq < 原生小程序 < vue系列 < angurle系列 < react系列
vue是剛開始一邊看着api就能夠擼出來的項目。
也許每一個框架都有自身的bug。有bug不可怕,怕的是沒有解決方案。而vue中,你卡到問題點,但本身沒有能力解決時,活躍的社區會給你答案。
支持axios, webpack, sass,elemnt-ui,vuex, router 等第三方插件。
vue有着腳手架,ssr的nuxt框架,app版本的weex, 小程序多端開發uniapp。
可謂學好vue,吃遍前端全家桶。
最後:也許你以爲,上述react都支持。好的吧,的確是,晚些彙總完react的文章,再寫一篇對比。
vue-cli腳手架,幫咱們作了什麼。vue-cli3.0開始,已經成爲可選擇性的插件。咱們分析一下各個插件的做用。
blog.guowenfh.com/2016/03/24/…
webpack,打包全部的「腳本」。腳手架已經幫咱們經過webpack作了不少默認的loader。
咱們項目中,不一樣的文件,通過編譯輸出最終的html,js,css,都是通過webpack。
例如,編譯 ES2015 或 TypeScript 模塊成 ES5 CommonJS 的模塊;
再例如:編譯 SASS 文件成 CSS,而後把生成的CSS插入到 Style 標籤內,而後再轉譯成 JavaScript 代碼段,處理在 HTML 或 CSS 文件中引用的圖片文件,根據配置路徑把它們移動到任意位置,根據 MD5 hash 命名。
所以,咱們能夠不一樣文件,找在webpack不一樣的編譯器,如vue有vue-loader,腳手架幫咱們引入了。如sass有sass-loader,基本npm或者yarn生態圈中,已經有前端你全部見過的loader。也許還有沒有?不要緊,咱們能夠本身寫一個。
來個簡單的需求:開發環境過濾掉全部的打印。
這要是在傳統的項目,沒有通過編譯器,這是有多大的工做量。當有了咱們的webpack或者gulp等,他僅僅只是幾句代碼的問題。咱們來看一下webpack的實現:
配置文件:
const fs = require('fs');
function wzJsLoader(source) {
/*過濾輸出*/
const mode = process.env.NODE_ENV === 'production'
if( mode ){//正式環境
source = source.replace(/console.log\(.*?\);/ig, value => "" );
}
return source;
};
module.exports = wzJsLoader;
複製代碼
這樣,咱們就輕鬆定了一個本身的loader。在wepback.config.js,加上咱們對應的loader,輕鬆解決問題
{
test: /\.js$/, //js文件加載器
exclude: /node_modules/,
use: [
{
loader: 'babel-loader?cacheDirectory=ture',
options: {
presets: ['@babel/preset-env']
}
},
{
loader: require.resolve("./myWebPackConfig/wz-js-loader"),//添加的
options: { name: __dirname },
},
]
},
複製代碼
webpack是個頗有難度的東西,本文就不繼續簡介,簡單瞭解webpack的配置,以及如何寫好Loader跟plugins等。若是你還有精力深刻,webpack的執行機制,如何打包成文件,他的生命週期等,均可以深刻挑戰,若是研究透徹,相信你的實力不通常。
axios,網絡請求工具。提到網絡請求工具,你確定瞭解從ajax,fetch、axios。下邊這次講一下他們的發展史以及優缺點(具體何時,會在下文的「vue項目的二次封裝」中講解)
ajax,相信早期進入前端領域的人,都大爲喜歡。他基於jquery,對原生XHR的封裝,還支持JSONP,很是方便。 他的有點包括,無須要經過刷新頁面更新數據,支持異步與服務器通訊,並且規範被普遍支持。
當年可謂如「諾基亞」通常存在。惋惜「諾基亞」後來跌下神壇,ajax在網絡請求中也遭受的一樣的待遇。
那麼淘汰ajax的根本緣由是什麼呢?
由於引入的單頁面框架,如vue的mvvn架構,或者是隻有m的react,他們都屬於js驅動Html。這涉及到控制dom刷新的過程。es5能夠利用callback, 或者generater的迭代器模式進行處理。可是還不理想。因此es6引入了promise的概念。
因此,以返回promise的單位的異步控制進程逐步發展。
一方面,ajax沒有改進,他依然我行我素的不支持promise。這對「新」前端的理念很不符,咱們沒法用ajax來完成異步操做(除非回調地獄,寫過大項目的都知道定位問題太難了)。
另外一方面,他還須要引入jquery來實現。咱們都知道新框架,都基本脫離了jq。
SO,fetch就這樣產生了。解決了ajax沒法返回promise的問題。開始讓人拋棄ajax。
fetch號稱是ajax的替代品,它的API是基於Promise設計的,舊版本的瀏覽器不支持Promise,須要使用polyfill es6-promise
然而,fetch貌似是爲解決返回Promise而產生的,並無注意其餘網絡請求工具該作的細節,他雖然支持promise, 但暴露了太多的問題:
1)fetch只對網絡請求報錯,對400,500都當作成功的請求,服務器返回 400,500 錯誤碼時並不會 reject,只有網絡錯誤這些致使請求不能完成時,fetch 纔會被 reject。
2)fetch默認不會帶cookie,須要添加配置項: fetch(url, {credentials: 'include'})
3)fetch不支持abort,不支持超時控制,使用setTimeout及Promise.reject的實現的超時控制並不能阻止請求過程繼續在後臺運行,形成了流量的浪費
4)fetch沒有辦法原生監測請求的進度,而XHR能夠
所以,axios正式入場。他從新基於xhr封裝,支持返回Promise, 也解決了fetch的弊端。
反問:知道jquery,fetch,axios的區別了嗎?
在沒有「路由」的概念時,咱們一般講「頁面路徑」。若是你經歷過spring mvc經過action映射到html頁面的時代,那麼恭喜 ,你已經使用過路由。他屬於後臺路由。後臺的路由,能夠簡單的理解成一個路徑的映射。
那麼有後臺路由,就會有前端路由。沒錯,帶來質的改變,就是前端路由。那麼他帶來的優點是什麼。
前端路由,又分hash模式跟history模式。咱們用兩張圖來簡單的說明一下,前端路由的原理:
hash(#)是URL 的錨點,表明的是網頁中的一個位置,單單改變#後的部分,瀏覽器只會滾動到相應位置,不會從新加載網頁,也就是說hash 出如今 URL 中,但不會被包含在 http 請求中,對後端徹底沒有影響,所以改變 hash 不會從新加載頁面;同時每一次改變#後的部分,都會在瀏覽器的訪問歷史中增長一個記錄,使用」後退」按鈕,就能夠回到上一個位置;因此說Hash模式經過錨點值的改變,根據不一樣的值,渲染指定DOM位置的不一樣數據。hash 模式的原理是 onhashchange 事件(監測hash值變化),能夠在 window 對象上監聽這個事件。
優點呢?是否是很明顯?若是沒有使用異步加載,咱們的已經能夠不須要通過後臺,直接僅是頁面的「錨點」切換。
history模式充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。這兩個方法應用於瀏覽器記錄棧,在當前已有的 back、forward、go 基礎之上,它們提供了對歷史記錄修改的功能。只是當它們執行修改時,雖然改變了當前的 URL ,但瀏覽器不會當即向後端發送請求。
history模式充分利用 history.pushState API 來完成 URL 跳轉而無須從新加載頁面。
**此外,**vue的路由,還支持嵌套(多級)路由,支持路由動態配置,命名視圖(同一頁面多個路由),路由守衛, 過渡動態效果等,可謂功能十分之強大,考慮比較齊全,在此每一個列舉一個簡單的栗子:
路由動態配置:
const router = new VueRouter({
routes: [
動態路徑參數 以冒號開頭
{ path: '/detail/:id', component: Detail }
]
})
複製代碼
嵌套(多級)路由: const router = new VueRouter({
routes: [
{ path: '/detail/', component: User,
children: [
{
path: 'product',
component: Product //二級嵌套路由
},
]
}
]
})
複製代碼
命名視圖:
<router-view></router-view>
<router-view name="a"></router-view>
<router-view name="b"></router-view>
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: componentsDefulat,
a: componentsA,
b: componentsB
}
}
]
})
複製代碼
路由守衛:
router.beforeEach((to, from, next) => {
// ...
})
複製代碼
動態效果:
<transition>
<router-view></router-view>
</transition>
複製代碼
sass跟less,二者都是CSS預處理器的佼佼者。
爲何要使用CSS預處理器?
CSS有具體如下幾個缺點:
1.語法不夠強大,好比沒法嵌套書寫,致使模塊化開發中須要書寫不少重複的選擇器;
2.沒有變量和合理的樣式複用機制,使得邏輯上相關的屬性值必須以字面量的形式重複輸出,致使難以維護。
Less和Sass在語法上有些共性,好比下面這些:
一、混入(Mixins)——class中的class;
二、參數混入——能夠傳遞參數的class,就像函數同樣;
三、嵌套規則——Class中嵌套class,從而減小重複的代碼;
四、運算——CSS中用上數學;
五、顏色功能——能夠編輯顏色;
六、名字空間(namespace)——分組樣式,從而能夠被調用;
七、做用域——局部修改樣式;
八、JavaScript 賦值——在CSS中使用JavaScript表達式賦值。
再說一下二者的區別:
1.Less環境較Sass簡單,使用起來較Sass簡單
2.從功能出發,Sass較Less略強大一些 (1) sass有變量和做用域。
(2) sass有函數的概念;
(3) sass能夠進行進程控制。例如: -條件:@if @else; -循環遍歷:@for @each @while
(4) sass又數據結構類型: -list類型=數組; -map類型=object; 其他的也有string、number、function等類型
3.Less與Sass處理機制不同
前者是經過客戶端處理的,後者是經過服務端處理,相比較之下前者解析會比後者慢一點。並且sass會產生服務器壓力。
vuex官方概念:Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。
看到這裏你可能會有疑問,咱們傳統的框架上,localstore, session , cookies,不就以及解決問題了麼。
沒錯。他們是解決了本地存儲的問題。可是vue是單頁面架構,須要數據驅動。 session , cookies沒法觸發數據驅動。這時候不得引入一個能夠監聽的容易。小型項目可能直接用store,或者頁面與頁面直接能夠用props傳遞
咱們在使用Vue.js開發複雜的應用時,常常會遇到多個組件共享同一個狀態,亦或是多個組件會去更新同一個狀態,在應用代碼量較少的時候,咱們能夠組件間通訊去維護修改數據,或者是經過事件總線來進行數據的傳遞以及修改。可是當應用逐漸龐大之後,代碼就會變得難以維護,從父組件開始經過prop傳遞多層嵌套的數據因爲層級過深而顯得異常脆弱,而事件總線也會由於組件的增多、代碼量的增大而顯得交互錯綜複雜,難以捋清其中的傳遞關係。
那麼爲何咱們不能將數據層與組件層抽離開來呢?把數據層放到全局造成一個單一的Store,組件層變得更薄,專門用來進行數據的展現及操做。全部數據的變動都須要通過全局的Store來進行,造成一個單向數據流,使數據變化變得「可預測」。
簡單說一下他的工做流程:
圖文相信已經很是清晰vuex的工做流程。簡單的簡述一下api:
state 簡單的理解就是vuex數據的儲存對象。
getters getter 會暴露爲 state 對象,你能夠以屬性的形式訪問這些值:
actions Action 相似於 mutation,不一樣在於: Action 提交的是 mutation,而不是直接變動狀態。 Action 能夠包含任意異步操做。
mutations 每一個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。 mutations能夠直接改變state的狀態。 mutations 不能夠包含任意異步操做
module Vuex 太大時,容許咱們將 store 分割成模塊(module)。每一個模塊擁有本身的 state、mutation、action、getter。
vuex的使用,很簡單。可是靈活時候,可能還須要進一步的瞭解源碼。vuex的原理其實跟vue有點像。
如須要看vuex源碼,可經過:github.com/zhuangweizh…
即UI庫的選擇
vue的火熱,離不開vue社區的火熱。就常規的項目,若是公司不是要求特別高,基本各類UI庫,已經不須要你寫樣式(前端最煩的就是寫樣式沒意見吧)。
這裏就不作UI庫如何搭建的文章,有興趣能夠關注,後續我會寫一篇專門搭建UI庫的。
這裏介紹一下vue火熱的UI庫吧。
其中,移動端筆者推薦vant,管理後臺推薦element。
上文講解過axios的由來以及優缺點,這裏談談axios在vue項目的使用。
1)請求攔截
好比咱們的請求接口,全局都須要作token驗證。咱們能夠在請求錢作好token雁陣。若是存在,則請求頭自動添加token。
axios.interceptors.request.use(
config => {
// 每次發送請求以前判斷vuex中是否存在token
// 若是存在,則統一在http請求的header都加上token,這樣後臺根據token判斷你的登陸狀況
// 即便本地存在token,也有可能token是過時的,因此在響應攔截器中要對返回狀態進行判斷
const token = store.state.token;
token && (config.headers.token = token);
return config;
},
error => {
return Promise.error(error);
})
複製代碼
2)返回攔截
當程序異常的時候呢,接口有時候在特定的場景,或者是服務器異常的狀況下,是否就讓用戶白白等待? 若是有超時,錯誤返回機制,及時告知用戶的,是否是用戶好一點?這就是返回的攔截。
axios.interceptors.response.use(
response => {
// 若是返回的狀態碼爲200,說明接口請求成功,能夠正常拿到數據
// 不然的話拋出錯誤
if (response.status === 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
// 服務器狀態碼不是2開頭的的狀況
// 這裏能夠跟大家的後臺開發人員協商好統一的錯誤狀態碼
// 而後根據返回的狀態碼進行一些操做,例如登陸過時提示,錯誤提示等等
// 下面列舉幾個常見的操做,其餘需求可自行擴展
error => {
alert("數據異常,請稍後再試或聯繫管理員");
return Promise.reject(error.response);
}
}
});
複製代碼
3)以get爲栗子
export function get(url, params){
return new Promise((resolve, reject) =>{
axios.get(url, {
params: params
}).then(res => {
resolve(res.data);
}).catch(err =>{
reject(err.data)
})
});
複製代碼
此外,對axios的使用還有想法的,建議查看一下axios全攻略: ykloveyxk.github.io/2017/02/25/…
上文曾提到,vue-cli自帶webpack。那麼咱們如何經過他,來改進咱們的項目呢。
從環境區分,自帶的引入,已經幫咱們區分了環境,而後幫咱們導入不一樣的loader跟Pulger等,基本已是一個很是完善的編譯器。
咱們見到看一下dev的源碼(添加了註釋),dev環境,實際上會運行dev-server.js文件該文件以express做爲後端框架
// nodejs環境配置
var config = require('../config')
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn') //強制打開瀏覽器
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware') //使用代理的中間件
var webpackConfig = require('./webpack.dev.conf') //webpack的配置
var port = process.env.PORT || config.dev.port //端口號
var autoOpenBrowser = !!config.dev.autoOpenBrowser //是否自動打開瀏覽器
var proxyTable = config.dev.proxyTable //http的代理url
var app = express() //啓動express
var compiler = webpack(webpackConfig) //webpack編譯
//webpack-dev-middleware的做用
//1.將編譯後的生成的靜態文件放在內存中,因此在npm run dev後磁盤上不會生成文件
//2.當文件改變時,會自動編譯。
//3.當在編譯過程當中請求某個資源時,webpack-dev-server不會讓這個請求失敗,而是會一直阻塞它,直到webpack編譯完畢
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
//webpack-hot-middleware的做用就是實現瀏覽器的無刷新更新
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: () => {}
})
//聲明hotMiddleware無刷新更新的時機:html-webpack-plugin 的template更改以後
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
//將代理請求的配置應用到express服務上
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
//使用connect-history-api-fallback匹配資源
//若是不匹配就能夠重定向到指定地址
app.use(require('connect-history-api-fallback')())
// 應用devMiddleware中間件
app.use(devMiddleware)
// 應用hotMiddleware中間件
app.use(hotMiddleware)
// 配置express靜態資源目錄
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))
var uri = 'http://localhost:' + port
//編譯成功後打印uri
devMiddleware.waitUntilValid(function () {
console.log('> Listening at ' + uri + '\n')
})
//啓動express服務
module.exports = app.listen(port, function (err) {
if (err) {
console.log(err)
return
}
// 知足條件則自動打開瀏覽器
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
})
複製代碼
可見,webpack的編譯,以及相對完善。咱們也能夠去優化一下對應的插件,好比:
plugins: [
new webpack.DefinePlugin({ // 編譯時配置的全局變量
'process.env': config.dev.env //當前環境爲開發環境
}),
new webpack.HotModuleReplacementPlugin(), //熱更新插件
new webpack.NoEmitOnErrorPlugin(), //不觸發錯誤,即編譯後運行的包正常運行
new HtmlWebpackPlugin({ //自動生成html文件,好比編譯後文件的引入
filename: 'index.html', //生成的文件名
template: 'index.html', //模板
inject: true
}),
new FriendlyErrorsPlugin() //友好的錯誤提示
]
複製代碼
最後講一下webpack的相關優化:
1.HappyPack 基於webpack的編譯模式本是單線程,時間佔時最多的Loader對文件的轉換。開啓HappyPack,能夠講任務分解成多個進程去並行處理。
簡單配置:
new HappyPack({// 用惟一的標識符 id 來表明當前的 HappyPack 是用來處理一類特定的文件 id: 'babel',// 如何處理 .js 文件,用法和 Loader 配置中同樣 loaders: ['babel-loader?cacheDirectory'],// ... 其它配置項 }),
詳細可參考:www.fly63.com/nav/1472
2.DllPlugin 可將一些Node_moudle一些編譯好的庫,經常使用並且不變的庫,抽出來。這樣就無需從新編譯。
3.Loader 記錄配置搜索範圍,include,exclude 如:
{ test: /.js$/, //js文件加載器 exclude: /node_modules/, use: [ { loader: 'babel-loader?cacheDirectory=ture', options: { presets: ['@babel/preset-env'] }, include: Path2D.resolve(__dirname, 'src') } ] }
1.tree shaking寫法。(webpack 4 已自動引入) 即「搖樹」。即只引入所須要引入部分,其他的代碼講會過濾。
2.壓縮代碼 當前最程愫的壓縮工具是UglifyJS。HtmlWebpackPlugin也可配置minify等。
3.文件分離 多個文件加載,速度更快。 例如:mini-css-extract-plugin,將css獨立出來。這樣還有利於,部分「不變」的文件作緩存。
4.Scope Hoisting 開啓後,分細分出模塊直接的依賴關係,會自動幫咱們合併函數。簡單的配置:
module,exports={ optimization:{ concatenateModules:true } }
任何框架,團隊都須要本身的組件化。(固然,有些團隊,怕人員的流動性,所有不組件化,最簡單的寫法,筆者也遇過這種公司)。
通常來講,組件大體能夠分爲三類:
關於1),能夠理解成如今的UI庫(如element/vant),這裏暫時不作獨立組件分析。(晚些可能會寫一篇如何寫獨立組件的文章,上傳到npm。)
關於2),貌似當某一個模塊,頁面須要屢次重複使用時候,就能夠寫成獨立組件,這個貌似沒什麼好分析。
這裏重點分析一下:** 3)業務上可複用的基礎組件 ** 。
筆者寫過的vue項目,都基本會封裝20~30個業務通用組件。例如截圖的my-form,my-table。以下:
這裏我覺得myTable
emelent的table插件,的確已經很強大了。可是筆者雖然用上了emelent ui,可是業務代碼卻沒有任何emelent的東西。
若是有一天,公司再也不喜歡element ui的table,那so easy,我把個人mytable修改一下,全部頁面即將同步。這就是組件化的魅力。
下邊我以my-table爲栗子,記錄一下我組件化的要點: 1.合併封裝分頁,是表格再也不關心分頁問題。 2.統一全局表格樣式(後期可隨時修改) 3.業務脫離,使業務上無需再關心element的api如何定義,且可隨時替換掉element。 4.自定義類型,本文提供select跟text控制,配置對象便可實現。 5.統一自定義缺省處理。 6.統一搜索按鈕,搜索框。配置對象便可實現。
這些優點,以及對全局的拓展性,是否是比傳統直接用的,有很大的優點?
固然,很差的地方,插件應該相對完善,考慮周全,須要一個全局統籌的人。對人員的流動的公司,的確很不友好。
下邊是源碼提供,可參考:
<template>
<div>
<h3 class="t_title">{{tName}}</h3>
<div class="t_content">
<el-form :inline="true" class="serach_form" >
<el-form-item v-for="(item, index) in tSerachList" :label="item.name" :key="index" v-if="tSerachList.length > 0 ">
<div v-if="item.type == 'text'" >
<el-input :placeholder="item.name" v-model="tSerachList[index].value" ></el-input>
</div>
<div v-else-if="item.type == 'select'" >
<el-select v-model="tSerachList[index].value" :placeholder="item.name">
<el-option v-for="(cItem, cIndex) in item.list" :key="cIndex" :label="cItem.name" :value="cItem.value" ></el-option>
</el-select>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" >查詢</el-button>
</el-form-item>
</el-form>
<!--按鈕操做模塊-->
<el-row class="t_button_tab" v-for="(item,index) in tBtnOpeList" :key="index">
<el-button :type="item.type" @click="btnOpeHandle(item.opeList)" :render="item.render">{{item.label}}</el-button>
</el-row>
</div>
<div class="t_table">
<el-table
:data="tableData"
style="width: 100%">
<el-table-column v-for="(item, index) in tableList" :key="index" v-bind="item">
<template slot-scope="scope" >
<my-table-render v-if="item.render" :row="scope.row" :render="item.render" ></my-table-render>
<span v-else>{{scope.row[item.key]}}</span>
</template>
</el-table-column>
</el-table>
</div>
<div class="t_pagination">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-size="tPageSize"
layout="prev, pager, next, jumper"
:total="tTotal">
</el-pagination>
</div>
</div>
</template>
<script>
import MyTableRender from './my-table-render.vue'
export default {
props: {
tTablecolumn: { //展現的列名
type: Array,
required: true
},
tName: { //頁面的名稱
type: String,
required: true
},
tUrl: { //請求的URL
type: String,
required: true
},
tParam: { //請求的額外參數
type: Object,
required: true
},
tSerachList: { //接口的額外數據
type: Array,
required: true
},
tBtnOpeList: {
type: Array,
required: false
}
},
data () {
return {
arrea: "",
currentPage: 1,
tableData: [],
tableList: [],
tTotal: 0,
tPageSize: 10,
serachObj: {} //搜索的文本數據
}
},
created () {
this.getTableList()
this.reloadTableList()
},
methods: {
async getTableList () {
var Obj = { pageNum: this.currentPage, pageSize: this.tPageSize }
var that = this;
var url = this.tUrl;
var param = Object.assign(this.tParam, Obj, this.serachObj)
const res = await this.utils.uGet({ url:url, query:param })
var list = res.data.dataList
that.tableData = list
that.tTotal = res.data.total
},
// 提交
reloadTableList () {
var tableList = this.tTablecolumn
for (var i = 0; i < tableList.length; i++) {
tableList[i].prop = tableList[i].key
tableList[i].label = tableList[i].name
}
this.tableList = tableList
},
onSubmit ( res ) {
var that = this;
const status = this.$store.getters.getUserStatus;
if( status == 4 ){
this.utils.uErrorAlert("臨時用戶無權限哦");
} else {
this.utils.uLoading(800);
var tSerachList = this.tSerachList;
var obj = {}
for (var i = 0; i < tSerachList.length; i++) {
obj[ tSerachList[i].key ] = tSerachList[i].value;
}
this.serachObj = obj;
this.currentPage = 1;
this.getTableList();
}
},
handleSizeChange () {
},
handleCurrentChange (obj) {
this.currentPage = obj
this.getTableList()
},
btnOpeHandle(params){
const status = this.$store.getters.getUserStatus;
if( status == 4 ){
this.utils.uErrorAlert("臨時用戶無權限哦");
} else {
this.$emit('handleBtn', params);
}
}
},
components: {
MyTableRender
}
}
</script>
<style lang="scss">
@import '@/assets/scss/element-variables.scss';
.serach_form{
background: $theme-light;
text-align: left;
padding-top: 18px;
padding-left: 20px;
}
.t_title{
/*float: left;*/
/*padding: 20px;*/
/*font-size: 23px;*/
color:$theme;
text-align: left;
border-left: 3px solid $theme;
padding-left: 5px;
}
.t_content{
clear: both;
}
.t_table{
clear: both;
padding: 20px;
}
.t_pagination{
margin-top: 20px;
float: right;
margin-right: 20px;
}
.t_button_tab{
text-align: left;
margin-top: 18px;
}
</style>
複製代碼
最後送上我的手寫的mini版本vue源碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>wz手寫vue源碼</title>
</head>
<body>
<div id="app" class="body" >
<div class="b_header" >
<img class="b_img" src="https://user-gold-cdn.xitu.io/2020/7/10/173344e271bf85af?w=400&h=400&f=png&s=3451" /><span>wz手寫vue源碼</span>
</div>
<div class="b_content" >
<div class="n_name" >姓名:{{name}}</div>
<div class="box" >
<span>年齡:{{age}}</span>
</div>
<div>{{content}}</div>
<div>
<input type="text" wz-model="content" placeholder="請輸入自我介紹" />
</div>
<div wz-html="htmlSpan" ></div>
<button @click="changeName" >點擊提示</button>
</div>
</div>
<style>
.body{
text-align: left;
width: 300px;
margin: 0 auto;
margin-top: 100px;
}
.body .b_header{
display: flex;
justify-item: center;
justify-content: center;
align-items: center;
align-content: center;
margin-bottom: 20px;
}
.body .b_header span{
font-size: 21px;
}
.body .b_img{
display: inline-flex;
width: 20px;
height: 20px;
align-item: center;
}
.body .b_content{
}
.body div{
margin-top: 10px;
min-height: 20px;
}
button{
margin-top: 20px;
}
</style>
<script src="./wzVue.js"></script>
<script>
const w = new wzVue({
el: '#app',
data: {
"name": "加載中...",
"age": '加載中...',
"content": "我是一枚優秀的程序員",
"htmlSpan": '<a href="http://wwww.zhuangweizhan.com">點擊歡迎進入我的主頁 </a>'
},
created() {
setTimeout(() => {
this.age = "25歲";
this.name = "weizhan";
}, 800);
},
methods: {
changeName() {
alert("歡迎進入我的主頁: http://www.zhuangweizhan.com");
}
}
})
</script>
</body>
</html>
// js文件
/*
本代碼來自weizhan
*/
class wzVue {
constructor(options){
this.$options = options;
console.log("this.$options===" + JSON.stringify(this.$options) );
this.$data = options.data;
this.$el = options.el;
this.observer( this.$data );//添加observer監聽
new wzCompile( options.el, this);//添加文檔解析
if ( options.created ) {
options.created.call(this);
}
}
observer( data ){//監聽data數據,雙向綁定
if( !data || typeof(data) !== 'object'){
return;
}
Object.keys(data).forEach(key => {//若是是對象進行解析
this.observerSet(key, data, data[key]);//監聽data對象
this.proxyData(key);//本地代理服務
});
}
observerSet( key, obj, value ){
this.observer(key);
const dep = new Dep();
Object.defineProperty( obj, key, {
get(){
Dep.target && dep.addDep(Dep.target);
return value;
},
set( newValue ){
if (newValue === value) {
return;
}
value = newValue;
//通知變化
dep.notiyDep();
}
})
}
proxyData(key){
Object.defineProperty( this, key, {
get(){
return this.$data[key];
},
set( newVal ){
this.$data[key] = newVal;
}
})
}
}
//存儲數據數組
class Dep{
constructor(){
this.deps = [];
}
addDep(dep){
this.deps.push(dep);
}
notiyDep(){
this.deps.forEach(dep => {
dep.update();
})
}
}
//我的編譯器
class wzCompile{
constructor(el, vm){
this.$el = document.querySelector(el);
this.$vm = vm;
if (this.$el) {
this.$fragment = this.getNodeChirdren( this.$el );
this.compile( this.$fragment);
this.$el.appendChild(this.$fragment);
}
}
getNodeChirdren(el){
const frag = document.createDocumentFragment();
let child;
while( (child = el.firstChild )){
frag.appendChild( child );
}
return frag;
}
compile( el ){
const childNodes = el.childNodes;
Array.from(childNodes).forEach( node => {
if( node.nodeType == 1 ) {//1爲元素節點
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach( attr => {
const attrName = attr.name;//屬性名稱
const attrVal = attr.value;//屬性值
if( attrName.slice(0,3) === 'wz-' ){
var tagName = attrName.substring(3);
switch( tagName ){
case "model":
this.wzDir_model( node, attrVal );
break;
case "html":
this.wzDir_html( node, attrVal );
break;
}
}
if( attrName.slice(0,1) === '@' ){
var tagName = attrName.substring(1);
this.wzDir_click( node, attrVal );
}
})
} else if( node.nodeType == 2 ){//2爲屬性節點
console.log("nodeType=====22");
} else if( node.nodeType == 3 ){//3爲文本節點
this.compileText( node );
}
// 遞歸子節點
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
})
}
wzDir_click(node, attrVal){
var fn = this.$vm.$options.methods[attrVal];
node.addEventListener( 'click', fn.bind(this.$vm));
}
wzDir_model( node, value ){
const vm = this.$vm;
this.updaterAll( 'model', node, node.value );
node.addEventListener("input", e => {
vm[value] = e.target.value;
});
}
wzDir_html( node, value ){
this.updaterHtml( node, this.$vm[value] );
}
updaterHtml( node, value ){
node.innerHTML = value;
}
compileText( node ){
if( typeof( node.textContent ) !== 'string' ) {
return "";
}
console.log("node.textContent===" + node.textContent );
const reg = /({{(.*)}})/;
const reg2 = /[^/{/}]+/;
const key = String((node.textContent).match(reg)).match(reg2);//獲取監聽的key
this.updaterAll( 'text', node, key );
}
updaterAll( type, node, key ) {
switch( type ){
case 'text':
if( key ){
const updater = this.updateText;
const initVal = node.textContent;//記錄原文本第一次的數據
updater( node, this.$vm[key], initVal);
new Watcher( this.$vm, key, initVal, function( value, initVal ){
updater( node, value, initVal );
});
}
break;
case 'model':
const updater = this.updateModel;
new Watcher( this.$vm, key, null, function( value, initVal ){
updater( node, value );
});
break;
}
}
updateModel( node, value ){
node.value = value;
}
updateText( node, value, initVal ){
var reg = /{{(.*)}}/ig;
var replaceStr = String( initVal.match(reg) );
var result = initVal.replace(replaceStr, value );
node.textContent = result;
}
}
class Watcher{
constructor( vm, key, initVal, cb ){
this.vm = vm;
this.key = key;
this.cb = cb;
this.initVal = initVal;
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
update(){
this.cb.call( this.vm, this.vm[this.key], this.initVal );
}
}
複製代碼
文章均爲原創手寫,寫一篇原創上萬字的文章,明白了筆者的不易。
若有錯誤但願指出。
後續,我會繼續react的總結。