用 Vue.js 架构 Webapp 进阶笔记

前言

第一个笨拙的 Cordova 工程终于告一段落,anyway,经历了72个版本迭代,是一个 Android 和 Ios 都称得上稳定的作品,也算对自己有一个交代。

接下来,依然是做 APP,这一次,必须在把架构做得更加成熟,不能给自己挖坑。。。

前面已经学习过 Vuejs, Angularjs 以及 Ionic ,并且都有写过程序练手,勉强算是入了门,但要完成一个完整的架构,毕竟我还有很多知识盲点,压力山大。

废话就说到这里,这是一个 Vue 笔记,关于Ionic, Angularjs 或是 Angular2,若时间充裕,我接下来会尝试以同样的要求进行架构。

架构要求

  1. 有条理的目录结构
  2. 功能组件化,尽量少的文件交集
  3. 开发环境下有方便的调试系统
  4. 严格的代码检查
  5. 能方便地进行代码编译和代码混淆压缩

细节扎记

  1. 初始化项目,定义目录结构

    截图1

    这是一个 cordova 工程,通过以下命令创建

     cordova create cordovaVue com.gxxsite.cordovavue CordovaVue
    

    vueproject是vue工程目录,发布生产时通过 webpack 打包到 www 里,再通过 cordova 发布 android 和 ios 工程

    截图2

    这是 vue 工程目录,通过以下命令创建

     vue init webpack vueproject
    

    src是主程序,assets存放img,css,font资源,components存放vue单文件组件,vuex存放公共变量(参考官方vuex的shopping实例),auth.js负责处理用户权限相关逻辑,filter.js定义工程用到的过滤器,router.js管理vue-router路由,``提供接口数据服务

    如果你连cordova和vue-cli都没安装,那么请参考下面的完整命令:

     sudo npm install -g cordova
     sudo npm install -g vue-cli
     cordova create cordovaVue com.gxxsite.cordovavue CordovaVue
     cd cordovaVue
     vue init webpack vueproject
     cd vueproject
     npm install
    
  2. 安装用到的官方组件和loader

    • vue-router

        npm install vue-router
      
    • vue-resource

        npm install vue-resource
      
    • vuex

        npm install vuex
      
    • sass-loader

        npm install sass-loader
      
  3. 把直接搬运的资源文件加进webpack的配置中

    修改文件 build/webpack.base.conf.js ,以eot,ttf,svg等字体文件为例(woff使用url-loader的方式,网上摘抄,原因不深究)

     module.exports = {
       module: {
         loaders: [
           { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&minetype=application/font-woff" },
           { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" }
         ]
       }
     }
    
  4. 定义组件和路由器

    由于用到vuex,避免所有文件都需要import,我决定用一个主组件带无数子组件的方式定义路由,这样store只需要在main.vue中import即可,但是actions还是需要在每个组件中都import才能用

    router.js

     router.map({
       '/': {
         component: Main,
         subRoutes: {
           '/login': {
             name: 'login',
             component: Login
           },
           '/list': {
             name: 'list',
             component: List
           },
           '/detail/:id': {
             name: 'detail',
             component: function (resolve) {
               require(['./components/Detail.vue'], resolve)
             }
           }
         }
       }
     })
     router.redirect({
       '/': '/list'
     })
     router.start(App, 'app')
    

    App.vue

     <template>
       <router-view></router-view>
     </template>
     
     <script>
     export default {
     }
     </script>
    

    Main.vue

     <template>
       <div class="footer">
         <a v-link="{name: 'list'}"><i class="icon-home"></i><br /><span>首页</span></a>
         <a v-link="{name: 'login'}"><i class="icon-wode1"></i><br /><span>我的</span></a>
       </div>
       <div class="frame">
         <router-view></router-view>
       </div>
     </template>
     
     <script>
     import store from '../vuex/store'
     export default {
       store
     }
     </script>
     
     <style lang="scss">
     @import "../assets/css/common";
     .footer{}
     .frame{}
     </style>
    
  5. 定义跨组件公共状态 vuex

    vuex原理并不复杂,但使用方式是需要学习的,看懂官方提供的例子即可。

    github工程:https://github.com/vuejs/vuex/
    文档:http://vuejs.github.io/vuex/en/index.html

    我定义的目录结构:
    截图3

    store.js

     import Vue from 'vue'
     import Vuex from 'vuex'
     import footer from './modules/footer'
     import message from './modules/message'
     import listoption from './modules/listoption'
     
     Vue.use(Vuex)
     
     // create the store
     export default new Vuex.Store({
       modules: {
         footer,
         message,
         listoption
       }
     })
    

    基本是完全按照官方购物车例子的模式来实现的,文件粘贴出来意义也不大。

  6. 数据服务的抽离

    思考良久,还是确定把数据服务抽离出来,这里有两种可能情况,第一种情况是数据服务由后台人员以既定的方式统一定义,与后台的json接口一一对应,这样的话抽离出来是更合理一些,另一种情况是数据服务由每个前端全栈独立实现,这样的话在vue单文件组件里各自实现更合理一些。

    好吧,还是决定抽离出来,因为第一种情况更符合我的想法

    service.js

     export const articleService = {
       getArticleById: function (context, id) {
         return context.$http.get('getItem.json', {id: id}).then((response) => {
           return response.data.Data
         }, (response) => {
           // error callback
         })
       },
       getAllArticles: function (context) {
         return context.$http.get('getItems.json').then((response) => {
           return response.data.Data
         }, (response) => {
           // error callback
         })
       }
     }
    
  7. 组件间切换的既定流程

    利用vue-router提供的勾子实现既定流程

    activate勾子-检查权限 > data勾子发送 ajax 获取 data 的请求 > 渲染页面(通过$loadingRouteData实现loading)

    detail.vue

     <template>
       <div class="detail">
         <div v-if="$loadingRouteData">Loading ...</div>
         <div v-if="!$loadingRouteData">
           <h1>Detail - {{item.id}}</h1>
           {{item.title}}
           <br />
           {{item.createtime}}
           <br />
           <ul>
             <li v-for="item in otheritems">
               <a v-link="{name:'detail',params:{id:item.id}}">{{item.title}} - {{item.createtime}}</a>
             </li>
           </ul>
         </div>
       </div>
     </template>
     
     <script>
     import auth from '../auth.js'
     import { articleService } from '../service.js'
     
     export default {
       route: {
         activate: function (transition) {
           console.log('detail activated!')
           console.log(transition)
           if (auth.user.authenticated) {
             transition.next()
           } else {
             transition.abort('用户未登录')
             this.$route.router.go({name: 'login'})
           }
         },
         deactivate: function (transition) {
           console.log('detail deactivated!')
           transition.next()
         },
         data ({to: {params: { id }}}) {
           return Promise.all([
             articleService.getArticleById(this, id),
             articleService.getAllArticles(this)
           ]).then(([item, otheritems]) => ({ item, otheritems }))
         }
       },
       data: function () {
         return {
           item: {},
           otheritems: []
         }
       }
     }
     </script>
     
     <style lang="scss">
     </style>
    
  8. 调试方式

    vue初始化的工程已经通过提供了相当完善的调试环境,通过npm run dev命令运行,通过http://localhost:8080访问,但是eslint的检查严格得有点变态,如果需要修改,请修改.eslintrc.js文件,具体规则参考官网

  9. 生产方式

    vue初始化的工程提供了webpack的打包命令npm run build,可以并没有提供es6的语法支持,需要手动修改一下

    build/webpack.base.conf.js 修改内容

     module.exports = {
       module: {
         loaders: [
           {
             test: /\.js$/,
             loader: 'babel',
             include: projectRoot,
             exclude: /node_modules/,
             query: {
               presets: ['es2015'],
               plugins: ['transform-runtime']
             }
           }
         ]
       }
     }
    

    另外,修改output使发布文件夹是同级的www文件夹,并修改publicPath为相对路径,即将前面的/删除,符合cordova工程需要

     module.exports = {
       output: {
         path: path.resolve(__dirname, ‘../../www/static'),
         publicPath: 'static/',
         filename: '[name].js'
       }
     }
    

    有一个BUG,在build发布生产后,会导致vue文件中的css引用的图片相对路径多了一个static文件夹,绝对路径就变成两层static,是错误的,解决方法如下

    修改build/webpack.prod.conf.js,把以下代码中的extract设置为false

     vue: {
       loaders: cssLoaders({
         sourceMap: SOURCE_MAP,
         extract: false
       })
     },
    
  10. cordova 相关

    首先,修改index.html,加入cordova.js

    <script src="cordova.js"></script>
    

    然后,在main.js中修改启动方式为deviceready,为了兼顾调试,加个if

    if ('ontouchstart' in window) {
      document.addEventListener('deviceready', function () {
        router.start(App, 'app')
      }, false)
    } else {
      router.start(App, 'app')
    }
    

难点

  1. 要使用系统对象,如localStorage,或者是cordova插件定义的全局对象,请使用window.localStorage的方式,否则,逃不过代码检查。。。各种 not defined

  2. 在组件中手动操作路由,请使用对象this.$route.router,而非this.$route,以跳转为例this.$route.router.go('login')

  3. 在router提供的activate勾子中进行权限拦截,在跳转前需要通过transtion.abort()中止路由变更,否则该路径会被加入历史记录,造成污染

若您觉得我的博文对您有帮助,欢迎点击下方按钮对我打赏
打赏