基于 Webpack & Vue & Vue-Router 的 SPA 初体验

uaqc5392 8年前
   <p>最近这几年的前端圈子,由于戏台一般精彩纷呈,从 MVC 到 MVVM,你刚唱罢我登场。 backbone,angularjs 已成昨日黄花,reactjs 如日中天,同时另一更轻量的 vue 发展势头更猛,尤其是即将 release 的2.0版本,号称兼具了 angularjs 和 reactjs 的两者优点。不过现在的官方版本还是1.0 ,下面就是基于1.0版本的初体验。</p>    <h2><strong>1. 为什么要 SPA?</strong></h2>    <p><strong>SPA:</strong>就是俗称的单页应用(Single Page Web Application)。</p>    <p>在移动端,特别是 hybrid 方式的H5应用中,性能问题一直是痛点。 使用 SPA,没有页面切换,就没有白屏阻塞,可以大大提高 H5 的性能,达到接近原生的流畅体验。</p>    <h2><strong>2. 为什么选择 vue?</strong></h2>    <p>在选择 vue 之前,使用 reactjs 也做过一个小 Demo,虽然两者都是面向组件的开发思路,但是 reactjs 的全家桶方式,实在太过强势,而自己定义的 JSX 规范,揉和在 JS 的组件框架里,导致如果后期发生页面改版工作,工作量将会巨大。</p>    <p>vue 相对来说,就轻量的多,他的view层,还是原来的 dom 结构,除了一些自定义的 vue 指令作为自定义标签以外,只要学会写组件就可以了,学习成本也比较低。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3ba80962f18e16d8b012930b6b58d8b7.png"></p>    <h2><strong>3. 环境配置</strong></h2>    <p>初始化工程,需要 node 环境使用 npm 安装相应的依赖包。</p>    <p>先创建一个测试目录,在里面依次输入以下命令。</p>    <pre>  <code class="language-javascript">//初始化package.json  npm init    //安装vue的依赖  npm install vue --save  npm install vue-router --save    //安装webpack的开发依赖  npm install webpack --save-dev    //安装babel的ES6 Loader 的开发依赖  npm install babel --save-dev  npm install babel-core --save-dev  npm install babel-loader --save-dev  npm install babel-preset-es2015 --save-dev    //安装html loacer 的开发依赖  npm install html-loader --save-dev</code></pre>    <h2><strong>4. 目录结构</strong></h2>    <p>src 为开发目录,其中 components 为组件子目录,templates 为模板子目录。</p>    <p>dist 为构建出的文件目录。</p>    <p>index.html 为入口文件。</p>    <p>package.json 为项目描述文件,是刚才 npm init 所建立。</p>    <p>webpack.config.js 是 webpack 的构建配置文件</p>    <p><img src="https://simg.open-open.com/show/aca3091d7e13308239da896ccfef7e50.png"></p>    <h2><strong>5. Webpack 配置</strong></h2>    <p>下面是 webpack 的配置文件,如何使用 webpack,请移步 webpack 的官网。</p>    <pre>  <code class="language-javascript">var webpack= require("webpack");    module.exports={   entry:{    bundle:[ "./src/app.js"]   },   output:{    path:__dirname,    publicPath:"/",    filename:"dist/[name].js"   },   module:{    loaders:[        {test: /\.html$/, loaders: ['html']},     {test: /(\.js)$/, loader:["babel"] ,exclude:/node_modules/,       query:{        presets:["es2015"]      }        }    ]   },   resolve:{   },   plugins:[     /*      new webpack.optimize.UglifyJsPlugin({     compress: {      warnings: false     }    })                 */   ]  }</code></pre>    <h2><strong>6. 入口文件</strong></h2>    <p>index.html</p>    <pre>  <code class="language-javascript"><!doctype html>  <html lang="en">  <head>      <meta charset="UTF-8">      <title>Vue Router Demo</title>  </head>  <body>     <div id="app">        <router-view></router-view>      </div>      <script src="dist/bundle.js"></script>  </body>  </html></code></pre>    <p>其中 id 为 app 的 div 是页面容器,其中的 router-view 会由 vue-router 去渲染组件,讲结果挂载到这个 div 上。</p>    <p>app.js</p>    <pre>  <code class="language-javascript">var Vue = require('vue');  var VueRouter = require('vue-router');    Vue.use(VueRouter);  Vue.config.debug = true;  Vue.config.delimiters = ['${', '}']; // 把默认的{{ }} 改成ES6的模板字符串 ${ }  Vue.config.devtools = true;    var App = Vue.extend({});  var router = new VueRouter({});    router.map(require('./routes'));  router.start(App, '#app');  router.go({"path":"/"});</code></pre>    <p>这是 vue 路由的配置。 其中由于习惯问题,我把 vue 默认的{{ }} 改成了的 ${ } ,总感觉这样看模板,才顺眼一些。</p>    <p>routes.js</p>    <pre>  <code class="language-javascript">module.exports = {    '/': {      component: require('./components/index')    },     '/list': {      component: require('./components/list')    },    '*': {      component: require('./components/notFound')    }  }</code></pre>    <h2><strong>7. 第一个组件</strong></h2>    <p>components/index.js</p>    <pre>  <code class="language-javascript">module.exports = {    template: require('../templates/index.html'),      ready: function () {    }  };</code></pre>    <p>templates/index.html</p>    <pre>  <code class="language-javascript"><h1>Index</h1>  <hr/>  <p>Hello World Index!</p></code></pre>    <p>执行 webpack 构建命令</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ce42e29fcc74037b9a7b6fa3f7631a81.jpg"></p>    <p>浏览器中访问:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6985f2f53db105688d47352a7d561e8f.png"></p>    <p>查看 bundle 源码:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d73784348e8e3fd379d868636d67b1b9.jpg"></p>    <p>发现 template 模板文件,已经被 webpack 打成字符串了。这其中,其实是 webpack 的 html-loader 起的作用</p>    <h2><strong>8. 组件之间跳转</strong></h2>    <p>修改刚才的 index 组件,增加一个跳转链接,不用 href 了,要用 vue 的指令 v-link。</p>    <pre>  <code class="language-javascript"><h1>Index</h1>  <hr/>  <p>Hello World Index!</p>  <p><a v-link="{path:'/list'}" >List Page</a></p></code></pre>    <p>添加 list 组件</p>    <p>components/list.js</p>    <pre>  <code class="language-javascript">module.exports = {    template: require('../templates/list.html'),      data:function(){     return {items:[{"id":1,"name":"hello11"},{"id":2,"name":"hello22"}]};    },    ready: function () {    }  };</code></pre>    <p>templates/list.html</p>    <pre>  <code class="language-javascript"><h1>List</h1>  <hr/>    <p>Hello List Page!</p>  <ul>   <li v-for="(index,item) in items">        ${item.id} : ${item.name}   </li>  </ul></code></pre>    <p>v-for 也是 vue 的默认指令,是用来循环数据列表的。</p>    <p>现在开始执行 webpack --watch 命令进行监听,这样就不用每次敲 webpack 命令了。只要开发者每次修改 js 点了保存,webpack 都会自动构建最新的 bundle 文件。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c16e8534b9b074f64f7e16db9ddbf1c6.png"></p>    <p>浏览器里试试看:</p>    <p>index 页</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/83a59df4105a908bc2c974677eb428bc.png"></p>    <p>点击 List Page 跳转到 list 页</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7b3cc82e6867a433fd6a834e47f3e724.png"></p>    <p>Bingo! 单页面两个组件之间跳转切换成功!</p>    <h2><strong>9. 组件生命周期</strong></h2>    <p>修改 **componets/list.js **</p>    <pre>  <code class="language-javascript">module.exports = {    template: require('../templates/list.html'),      data:function(){     return {items:[{"id":1,"name":"hello11"},{"id":2,"name":"hello22"}]};    },        //在实例开始初始化时同步调用。此时数据观测、事件和 watcher 都尚未初始化    init:function(){     console.log("init..");    },      //在实例创建之后同步调用。此时实例已经结束解析选项,这意味着已建立:数据绑定,计算属性,方法,watcher/事件回调。但是还没有开始 DOM 编译,$el 还不存在。    created:function(){     console.log("created..");    },      //在编译开始前调用。    beforeCompile:function(){      console.log("beforeCompile..");    },      //在编译结束后调用。此时所有的指令已生效,因而数据的变化将触发 DOM 更新。但是不担保 $el 已插入文档。    compiled:function(){      console.log("compiled..");    },       //在编译结束和 $el 第一次插入文档之后调用,如在第一次 attached 钩子之后调用。注意必须是由 Vue 插入(如 vm.$appendTo() 等方法或指令更新)才触发 ready 钩子。    ready: function () {      console.log("ready..");      },      //在 vm.$el 插入 DOM 时调用。必须是由指令或实例方法(如 $appendTo())插入,直接操作 vm.$el 不会 触发这个钩子。    attached:function(){      console.log("attached..");    },      //在 vm.$el 从 DOM 中删除时调用。必须是由指令或实例方法删除,直接操作 vm.$el 不会 触发这个钩子。    detached:function(){      console.log("detached..");    },      //在开始销毁实例时调用。此时实例仍然有功能。    beforeDestroy:function(){      console.log("beforeDestroy..");    },      //在实例被销毁之后调用。此时所有的绑定和实例的指令已经解绑,所有的子实例也已经被销毁。如果有离开过渡,destroyed 钩子在过渡完成之后调用。    destroyed:function(){      console.log("destroyed..");    }    };</code></pre>    <p>在浏览器里执行了看看:</p>    <p>首次进入 List 页面的执行顺序如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/17901ab25feb0db3b66f0265e32bf357.jpg"></p>    <p>此时点一下浏览器的后退,List Component 会被销毁,执行顺序如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8b41ecd04fe6d7f6c096f006ae3434b5.jpg"></p>    <p>这是官方的生命周期的图:</p>    <p><img src="https://simg.open-open.com/show/c909347f181836bd29b545eda04e13e9.jpg"></p>    <h2><strong>10. 父组件与子组件</strong></h2>    <p>在很多情况下,组件是有父子关系的,比如 list 列表组件有个子组件 item</p>    <p>components/item.js</p>    <pre>  <code class="language-javascript">module.exports = {    template: require('../templates/item.html'),      props:["id","name"],        ready: function () {          }  };</code></pre>    <p>templates/item.html</p>    <pre>  <code class="language-javascript"><p>我是subitem: ${id} - ${name}</p></code></pre>    <p>修改 list 组件,添加 item 的引用</p>    <p>components/list.js</p>    <pre>  <code class="language-javascript">//引用item组件  import item from "./item";    module.exports = {    template: require('../templates/list.html'),      data:function(){     return {items:[{"id":1,"name":"hello11"},{"id":2,"name":"hello22"}]};    },       //定义item组件为子组件    components:{       "item":item    },      ready: function () {    }    };</code></pre>    <p>templates/list.html</p>    <pre>  <code class="language-javascript"><h1>List</h1>  <hr/>  <p>Hello List Page!</p>  <ul>   <li v-for="(index,item) in items">       <!--使用item子组件,同时把id,name使用props传值给item子组件-->       <item v-bind:id="item.id" v-bind:name="item.name"></item>   </li>  </ul></code></pre>    <p>浏览器里试试看:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b9322f076fb42757dd395f814af28538.png"></p>    <p>子组件成功被调用了</p>    <h2><strong>11. 组件跳转传参</strong></h2>    <p>组件之间的跳转传参,也是一种非常常见的情况。下面为列表页,增加跳转到详情页的跳转,并传参 id 给详情页</p>    <p>修改路由 routes.js</p>    <pre>  <code class="language-javascript">module.exports = {      '/': {      component: require('./components/index')    },     '/list': {      component: require('./components/list')    },      //增加详情页的跳转路由,并在路径上加上id传参,具名为name:show     '/show/:id': {        name:"show",        component: require('./components/show')      },    '*': {      component: require('./components/notFound')    }  }</code></pre>    <p>添加组件 show</p>    <p>components/show.js</p>    <pre>  <code class="language-javascript">module.exports = {    template: require('../templates/show.html'),      data:function(){     return {};    },      created:function(){      //获取params的参数ID      var id=this.$route.params.id;        //根据获取的参数ID,返回不同的data对象(真实业务中,这里应该是Ajax获取数据)      if (id==1){        this.$data={"id":id,"name":"hello111","age":24};      }else{          this.$data={"id":id,"name":"hello222","age":28};      }    },        ready: function () {     console.log(this.$data);    }    };</code></pre>    <p>templates/show.html</p>    <pre>  <code class="language-javascript"><h1>Show</h1>  <hr/>    <p>Hello show page!</p>    <p>id:${id}</p>  <p>name:${name}</p>  <p>age:${age}</p></code></pre>    <p>修改 templates/item.html</p>    <pre>  <code class="language-javascript"><p>我是subitem: <a v-link="{name:'show',params: { 'id': id } }"> ${id} : ${name}</a> </p></code></pre>    <p>这里 name:‘show’ 表示具名路由路径,params 就是传参。</p>    <p>继续浏览器里点到详情页试试:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8232d43d01a7491bff14a1239423c580.jpg"></p>    <p>点击“hello11”,跳转到详情页:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c3b4d53dee2a707428b8305dfb1b2d14.png"></p>    <p>传参逻辑成功。</p>    <h2><strong>12. 嵌套路由</strong></h2>    <p>仅有路由跳转是远远不够的,很多情况下,我们还有同一个页面上,多标签页的切换,在 vue 中,用嵌套路由,也可以非常方便的实现。</p>    <p>添加两个小组件</p>    <p>components/tab1.js</p>    <pre>  <code class="language-javascript">module.exports = {    template: "<p>Tab1 content</p>"  };</code></pre>    <p>components/tab2.js</p>    <pre>  <code class="language-javascript">module.exports = {    template: "<p>Tab2 content</p>"  };</code></pre>    <p>修改 components/index.js 组件,挂载这两个子组件</p>    <pre>  <code class="language-javascript">import tab1 from "./tab1";  import tab2 from "./tab2";    module.exports = {    template: require('../templates/index.html'),      components:{       "tab1":tab1,       "tab2":tab2    },      ready: function () {          }  };</code></pre>    <p>在路由里加上子路由</p>    <pre>  <code class="language-javascript">module.exports = {      '/': {      component: require('./components/index'),        //子路由      subRoutes:{        "/tab1":{            component:require('./components/tab1')        },        "/tab2":{            component:require('./components/tab2')        }      }    },       '/list': {      component: require('./components/list')    },       '/show/:id': {        name:"show",        component: require('./components/show')      },      '*': {      component: require('./components/notFound')    }    }</code></pre>    <p>好了,在浏览器里试一下:</p>    <p>初始状态:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4911724e0c7d4c8cc690ff862336d542.png"></p>    <p>点了 tab1,tab2:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ce26d86e8f0eabcadb468db92a728a44.png"></p>    <p>Tab 切换没问题,可是,初始状态显示是空的,能不能默认显示 Tab1 Content 呢?很简单,调整下路由就可以了:</p>    <pre>  <code class="language-javascript">module.exports = {      '/': {      component: require('./components/index'),        //子路由      subRoutes:{       //默认显示Tab1        "/":{            component:require('./components/tab1')        },        "/tab1":{            component:require('./components/tab1')        },        "/tab2":{            component:require('./components/tab2')        }      }    }  }</code></pre>    <p> </p>    <p> </p>    <p> </p>    <p>来自:http://dev.qq.com/topic/57d13a57132ff21c38110186</p>    <p> </p>