一、weex-hackernews介绍

Weex官方基于Weex和Vue开发了一个的完整项目,在项目中使用了Vuex和vue-router,能够实现同一份代码,在 iOS、Android、Web 下都能完整地工作。weex-hackernews的项目地址

1、下载

下载地址:https://github.com/weexteam/weex-hackernews
直接下载zip包下来

2、运行weex-hackernews

2.1 准备:

     1.搭建Weex本地开发环境,可以前往Weex官方按照开发文档教程进行搭建搭建地址:http://weex.apache.org/cn/guide/set-up-env.html
     2.下载开发工具:WebStorm、AndroidStudio、Android SDK、CocoaPods

2.2 安装

安装依赖:

npm install

编译代码:

# 生成 Web 平台和 native 平台可用的 bundle 文件
# 位置:
# dist/index.web.js
# dist/index.web.js
npm run build

# 监听模式的 npm run build
npm run dev

拷贝bundle文件:

# 将生成的 bundle 文件拷贝到 Android 项目的资源目录
npm run copy:android

# 将生成的 bundle 文件拷贝到 iOS 项目的资源目录
npm run copy:ios

# run both copy:andriod and copy:ios
npm run copy

# 注意:window系统下,修改下package.json文件,copy:android对应的命令行,官网下载下来的是mac系统命令行,要进行修改
修改前
"copy:android": "cp dist/index.weex.js android/app/src/main/assets/index.js"
修改后:
"copy:android":"xcopy.\\dist\\index.weex.js.\\android\\app\\src\\main\\assets\\index.js"

启动Web服务

npm run serve

启动服务后监听1337端口,访问 http://127.0.0.1:1337/index.html 即可在浏览器中预览页面

启动Android项目

     启动Android项目,首先安装Android Studio和Android SDK,并配置好基本的开发环境;用Android Studio 打开 android 目录的项目,等待自动安装完依赖以后,即可启动模拟器或者真机预览页面;

启动 iOS 项目

     启动 iOS 项目,首先应该配置好 iOS 开发环境 并且安装 CocoaPods 工具;进入 ios 目录,使用 CocoaPods 安装依赖;

pod install

使用 Xcode 打开ios目录中的项目(HackerNews.xcworkspace),然后即可启动模拟器预览页面。

注:如果想要在真机上查看效果,还需要配置开发者签名等信息。

2.3 运行效果图
  • 首页

    这里写图片描述

  • 具体详情页

    这里写图片描述

二、代码分析

1  功能目录

将项目导入WebStorm里,功能目录分析

|-- android                          // android工程
|-- dist                             // android工程
|   |--dist/index.web.js             //Web平台bundle文件
|   |--dist/index.weex.js            //native平台bundle文件
|-- ios                              // ios工程
|-- src                              //项目的vue文件
|   |--components                    //vue组件(封装组件)
|   |--filters                       //vue的过滤器
|   |--mixins                        //vue的mixins(混合)
|   |--store                         //vuex(vue的状态管理器)
|   |--views                         //视图
|   |--App.vue                       //主UI界面
|   |--entry.js                      //入口文件
|   |--router.js                     //vue的路由声明

|-- .babelrc                         // ES6语法编译配置
|-- package.json                     // 配置项目相关信息,通过执行 npm init 命令创建
|-- qrcode.jpg                       //二维码
|-- README.md                        // 项目说明
|-- webpack.config.js                // 程序打包配置

2  vue路由router

2.1  vue-router 介绍

vue-router是vue.js官方支持的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。

2.2  vue-router知识点

这里vue-router的知识点,这边就不进行阐述,因为官方上有详细的介绍:https://router.vuejs.org/zh-cn/,如果快速入手推荐看之前看过的文章:http://blog.csdn.net/sinat_17775997/article/details/52549123

2.2  项目路由分析

router.js文件

import Router from 'vue-router'
import StoriesView from './views/StoriesView.vue'
import ArticleView from './views/ArticleView.vue'
import CommentView from './views/CommentView.vue'
import UserView from './views/UserView.vue'

Vue.use(Router)

// Story view factory
function createStoriesView (type) {
  return {
    name: `${type}-stories-view`,
    render (createElement) {
      return createElement(StoriesView, { props: { type }})
    }
  }
}

export default new Router({
  // mode: 'abstract',
  routes: [
    { path: '/top', component: createStoriesView('top') },
    { path: '/new', component: createStoriesView('new') },
    { path: '/show', component: createStoriesView('show') },
    { path: '/ask', component: createStoriesView('ask') },
    { path: '/job', component: createStoriesView('job') },
    { path: '/article/:url(.*)?', component: ArticleView },
    { path: '/item/:id(\\d+)', component: CommentView },
    { path: '/user/:id', component: UserView },
    { path: '/', redirect: '/top' }
  ]
})
  • 首先导入Router
import Router from 'vue-router'
  • Vue注入router
Vue.use(Router)
  • router的路由配置,导入各种View
export default new Router({
  // mode: 'abstract',
  routes: [
    { path: '/top', component: createStoriesView('top') },
    { path: '/new', component: createStoriesView('new') },
    { path: '/show', component: createStoriesView('show') },
    { path: '/ask', component: createStoriesView('ask') },
    { path: '/job', component: createStoriesView('job') },
    { path: '/article/:url(.*)?', component: ArticleView },
    { path: '/item/:id(\\d+)', component: CommentView },
    { path: '/user/:id', component: UserView },
    { path: '/', redirect: '/top' }
  ]
})
  • router的入口路径是path’/’,这在后面分析App.vue会讲到。然而路由的路劲’/’重定向到’/top’,所以’/top’对应的文件才是真正App入口UI界面。
{ path: '/', redirect: '/top' }
  • 接着看对应着路径对应的’/top’对应的component.
{ path: '/top', component: createStoriesView('top') }

如果看完上面给的两个介绍router的链接,就知道path代表匹配路径,component对应组件的文件。可能大家都会问‘/top’对应明明是createStoriesView(‘top’),我们沿着createStoriesView(type)方法看下去:

function createStoriesView (type) {
  return {
    name: `${type}-stories-view`,
    render (createElement) {
      return createElement(StoriesView, { props: { type }})
    }
  }
}

其实会发现在‘/top’对应的文件是StoriesView,其中props对应是传入该组件的参数。

  • 路由跳转
    路由跳转有两种形式:声明和编程。
#声明:
<router-link :to="...">
#编程
router.push(...)
router.push({ path: 'home' }) 
router.push('home')
router.push({ name: 'user', params: { userId: 123 }})
//params跳转到组件传入参数key为userId,value为123
//跳转到的界面接收:this.$router.param.userId;

工程的路由的跳转封装在mixins/index.js文件中:

export default {
  methods: {
    jump (to) {
      if (this.$router) {
        this.$router.push(to)
      }
    }
  }
}

大家肯定疑问,mixins/index.js什么时候被注入到vue,能全局调用?其实在入口文件entry.js

// register global mixins.
Vue.mixin(mixins)

3  vue状态管理库Vuex

3.1  Vuex 介绍

Vuex是专为Vue.js应用程序开发的状态管理模型。它采用集中式存储应用的所有组件的状态(理想),并以相应规则保证状态已一种可预测的方式变化。Vuex也是集成到Vuex的官方调试工具

什么是”状态管理模式”?

包含以下几部分:

  • state,驱动应用的数据源
  • view,以声明方式将state映射到视图;
  • action,响应在view上的用户输入导致的状态变化

以下是一个表示“单向数据流”理念的示意

这里写图片描述

    Vuex的基本思想,借鉴Flu、Redux和The Elm Architechture,vuex是专门为Vue.js设计的状态管理库,利用响应体制来进行高效的状态更新。

Store
    每个Vuex的应用都有一个核心的store(仓库)。”store”是一个容器,里面存储应用的大部分的状态(state)。相当于一个全局对象,但是有跟全局对象有所区别的是,vuex的状态存储是响应式,只要store状态发生变化,那么相应的引用到的组件会跟着更新。
    store的核心内容由State、Getters、Mutations、Actions、Modules组成。

  • state:全局唯一数据源,定义着weex-hackernews工程的列表lists、用户users、items详情等数据;
  • getters:其实主要就是state数据处理,进行过滤操作;
  • mutation:唯一可以更改state里面的数据;
    -Actions:类似mutation,不同在于action提交的是mutation,而不是直接变更数据状态,Actions可以包含任意异步操作。

  • Modules:使用单一状态树,导致应用的所有状态集中到一个很大的对象。但是,当应用变得很大时,store 对象会变得臃肿不堪,Vuex 允许将 store 分割到模块(module),每个模块拥有自己的 state、mutation、action、getters.

        我们直接看下图官方的流程图,state作为全局数据源,我们通过dispatch触发action动作,action做业务处理在提交Mutation来改变State,State改变后自动Render到Vue的component组件上,从而实现单向数据流。
        看上面的理论大体懂个流程,有可能存在一知半解,后面直接通过项目的代码进行整个流程进行分析,到时基本可以明白Vuex的流程了。

4  入口文件

App.vue 文件

// import Vue from 'vue'
import App from './App.vue'                 //加载UI主界面
import router from './router'               //加载vue路由 
import store from './store'                 //加载vuex的store
import { sync } from 'vuex-router-sync'
import * as filters from './filters'        //加载vue的fitlter(过滤器)
import mixins from './mixins'                //加载vue的mixins(混合)

sync(store, router)

Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key])
})

Vue.mixin(mixins)

//vue扩展路由、状态管理器、入口UI主界面
new Vue(Vue.util.extend({ el: '#root', router, store }, App))             

router.push('/')     //默认路由跳转路径

该文件主要任务是路由(router)、状态管理器(store)、view的导入,
这边的路由的入口路径’/’

router.push('/')  

在router中的路由声明我们有提到过的路由的路口路径,就是在这边实现。

5  主界面

5.1  StoriesView主界面

StoriesView.vue
其实就是首页界面。
我们可以看到*.vue文件有三部分组成:<template>, <style>, <script> 构建

<template>
    .
    .
    .
</template>

<script>
    .
    .
    .
</script>

<style scoped>
    .
    .
    .
</style>

<template>必须的,主要是UI界面,使用 HTML 语法描述页面结构,内容由多个标签组成,不同的标签代表不同的组件。(weex限制范围内)
<script> 可选的,主要是业务逻辑,使用 JavaScript 描述页面中的数据和页面的行为(es6 的代码)
<style> 可选的,主要是样式,使用 CSS 语法描述页面的具体展现形式(weex限制范围内)
下面按照上面三部分解析StoriesView的代码:

  • script 业务逻辑
<script>
  //分别导入app-header、story组件  如果看不懂es6写法,建议先去看下补充下es6基础知识
  import AppHeader from '../components/app-header.vue'
  import Story from '../components/story.vue'

  export default {
    //声明组件   只有声明过的组件,在template才能使用,否则会报错
    components: { AppHeader, Story },
    //传参   我们可以看下之前路由router文件中提到的入参type传值,StoriesView的type就会被赋值
    props: {
      type: {
        type: String,            //type的数据类型
        required: true,          //必须元数
        default: 'top'           //默认值是top值
      }
    },
    //存放数据
    data () {
      return {
        loading: true
      }
    },
    //vue的计算属性
    computed: {
      //获取列表数据
      stories () {
        //从store获取数据
        return this.$store.getters.activeItems
      }
    },
    //使用方法
    methods: {
      //网络请求 列表接口数据
      fetchListData () {
        //加载状态 设置为显示
        this.loading = true
        //dispatch触发FETCH_LIST_DATA的action
        this.$store.dispatch('FETCH_LIST_DATA', {
          type: this.type
        }).then(() => {
          //加载状态 设置为隐藏
          this.loading = false
        })
      },
      //网络请求 加载更多>>列表接口数据
      loadMoreStories () {
        this.loading = true
        this.$store.dispatch('LOAD_MORE_ITEMS').then(() => {
          this.loading = false
        })
      }
    },
    //生命周期   组件实例创建完成,属性已绑定,但DOM还未生成
    created () {
      this.fetchListData()
    }
  }
</script>

fetchListData方法的里面使用到vuex,在此先不进行介绍,后面进行详说。

  • template UI界面
<template>
  <div class="stories-view" append="tree">
    <!--标题栏-->
    <app-header></app-header>
    <!--标题栏-->
    <!--list列表-->
    <list class="story-list" @loadmore="loadMoreStories" loadmoreoffset="50">
      <!--cell是list的item项 stories对应computed的stories() -->
      <cell class="story-cell" v-for="story in stories" :key="story.id" append="tree">
        <!--:story="story"  将数据story传参到story组件中-->
        <story :story="story"></story>
      </cell>
    </list>
    <!--list列表-->
    <!--加载更多控件-->
    <div class="loading" v-if="loading">
      <text class="loading-text">loading ...</text>
    </div>
    <!--加载更多控件-->
  </div>
</template>

标题栏

<app-header></app-header>

这里写图片描述

列表的item

<story :story="story"></story>

这里写图片描述

style 样式

<style scoped>
  .stories-view {
    height: 100%;
  }
  .story-cell {
    margin-bottom: 3px;
    border-bottom-width: 2px;
    border-bottom-style: solid;
    border-bottom-color: #DDDDDD;
    background-color: #FFFFFF;
  }
  .loading {
    width: 750px;
    height: 120px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .loading-text {
    margin: auto;
    text-align: center;
    font-size: 40px;
    color: #BBB;
  }
</style>

这边的样式就不详细介绍,直接官网直接看。

5.2  列表item

story.vue

<template>
    <div class="cell-item">
        <text class="story-score">{{story.score}}</text>
        <external-link :url="story.url" class="story-link">
            <text class="story-title">{{story.title}}</text>
            <text class="small-text" v-if="story.url">({{ story.url | host }})</text>
        </external-link>
        <div class="text-group">
            <text class="small-text text-cell">by</text>
            <!--jump 路由跳转-->
            <div class="text-cell" @click="jump(`/user/${story.by}`)">
                <text class="small-text link-text">{{story.by}}</text>
            </div>
            <text class="small-text text-cell"> | {{ story.time | timeAgo }} ago</text>
            <text class="small-text text-cell" v-if="!noComment"> |</text>
            <div class="text-cell" @click="jump(`/item/${story.id}`)" v-if="!noComment">
                <text class="small-text link-text">{{ story.descendants }} comments</text>
            </div>
        </div>
    </div>
</template>

<style scoped>
    .cell-item {
        position: relative;
        padding-top: 20px;
        padding-bottom: 25px;
        padding-left: 100px;
        padding-right: 40px;
    }

    .story-score {
        position: absolute;
        width: 100px;
        text-align: center;
        left: 0;
        top: 20px;
        font-size: 32px;
        font-weight: bold;
        color: #FF6600;
    }

    .story-link {
        margin-bottom: 25px;
        width: 610px;
    }

    .story-title {
        font-size: 33px;
        color: #404040;
    }

    .small-text {
        color: #BBB;
        font-size: 22px;
        margin-bottom: 0;
        font-family: Verdana, Geneva, sans-serif;
    }

    .link-text {
        /*color: red;*/
        text-decoration: underline;
    }

    .text-group {
        display: flex;
        flex-direction: row;
        flex-wrap: nowrap;
        justify-content: flex-start;
        align-items: center;
    }

    .text-cell {
        flex-grow: 0;
    }
</style>

<script>
    //导入控件
    import ExternalLink from './external-link.vue'

    export default {
        //控件声明
        components: {ExternalLink},
        //参数   列表的item数据
        props: {
            story: {
                type: Object,
                required: true
            },
            'no-comment': {
                type: [String, Boolean],
                default: false
            }
        }
    }
</script>

在template可以看到路由跳转方法jump

<div class="text-cell" @click="jump(`/user/${story.by}`)">

jump其实调取的是mixins(混合)文件夹下index.js的jump方法

this.$router.push(to)路由的跳转,to参数是对应的配置路径,会对应这router.js的路由表跳转到对应的vue;

5.3  标题栏

app-header.vue

<template>
  <div class="header">
    <!--@click="jump('/')"  点击事件>>>路由跳转:路径‘/’-->
    <div class="logo" @click="jump('/')">
      <!--src加载网络图片 地址:https://news.ycombinator.com/favicon.ico-->
      <image class="image" src="https://news.ycombinator.com/favicon.ico"></image>
    </div>
    <div class="nav">
      <div class="link" @click="jump('/top')">
        <text class="title">Top</text>
      </div>
      <div class="link" @click="jump('/new')">
        <text class="title">New</text>
      </div>
      <div class="link" @click="jump('/show')">
        <text class="title">Show</text>
      </div>
      <div class="link" @click="jump('/ask')">
        <text class="title">Ask</text>
      </div>
      <div class="link" @click="jump('/job')">
        <text class="title">Job</text>
      </div>
    </div>
  </div>
</template>

<style scoped>
  .header {
    position: relative;
    height: 120px;
    margin-bottom: 3px;
    border-bottom-width: 2px;
    border-bottom-style: solid;
    border-bottom-color: #DDDDDD;
    background-color: #FF6600;
  }
  .logo {
    position: relative;
    width: 50px;
    height: 50px;
    top: 35px;
    left: 35px;
    border-width: 3px;
    border-style: solid;
    border-color: #FFFFFF;
  }
  .image {
    width: 44px;
    height: 44px;
  }
  .nav {
    display: flex;
    position: absolute;
    left: 120px;
    top: 35px;
    flex-direction: row;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
  }
  .link {
    padding-left: 15px;
    padding-right: 15px;
  }
  .title {
    font-family: Verdana, Geneva, sans-serif;
    font-size: 32px;
    line-height: 44px;
    color: #FFFFFF;
  }
</style>

其他view就不再一一分析,都是大同小异。

6  数据store工程整体流程

6 .1 数据store工程整体流程

我们从一开始导入Vuex入手:

在entry入口文件

import store from './store

导入store,接着我们看下store的文件夹下的index.js文件。

首先,导入vuex插件

import Vuex from 'vuex

判断是否移动平台,是移动平台,将vuex插件导入vuex

if (WXEnvironment.platform !== 'Web') {
  Vue.use(Vuex)
}

而后实例化Store:

onst store = new Vuex.Store({
  actions,
  mutations,

  state: {
    activeType: null,
    items: {},
    users: {},
    counts: {
      top: 20,
      new: 20,
      show: 15,
      ask: 15,
      job: 15
    },
    lists: {
      top: [],
      new: [],
      show: [],
      ask: [],
      job: []
    }
  },

  getters: {
    activeIds (state) {
      const { activeType, lists, counts } = state
      return activeType ? lists[activeType].slice(0, counts[activeType]) : []
    },
    activeItems (state, getters) {
      return getters.activeIds.map(id => state.items[id]).filter(_ => _)
    }
  }
})

new Vuex.Store单例模式,里面分别注入state、actions、mutations、getters模块;state存储全局唯一数据源,定义着工程的列表lists、用户users、items详情等数据;
acitons和mutations分别在action.js和mutations文件中。
要理解vuex,我们直接拿获取列表数据做实例讲解:

网络获取数据赋值到store上

在StoriesView.vue文件

fetchListData () {
        this.loading = true
        this.$store.dispatch('FETCH_LIST_DATA', {
          type: this.type
        }).then(() => {
          this.loading = false
        })
      }

this.$store.dispatch(‘FETCH_LIST_DATA’,{type: this.type})触发actions里面的FETCH_LIST_DATA的动作,并传参数type值。

在actions.js文件

//第一个参数是store参数   第二个参数type是dispatch传参
export function FETCH_LIST_DATA ({ commit, dispatch, state }, { type }) {
  commit('SET_ACTIVE_TYPE', { type })
  return fetchIdsByType(type)
    .then(ids => commit('SET_LIST', { type, ids }))
    .then(() => dispatch('ENSURE_ACTIVE_ITEMS'))
}

第四行是调用mutation的SET_ACTIVE_TYPE方法,进行activeType的赋值;

export function SET_ACTIVE_TYPE (state, { type }) {
  state.activeType = type
}

继续看下fetchIdsByType()方法,fetchIdsByType()方法其实是从fetch文件导入

import { fetchItems, fetchIdsByType, fetchUser } from './fetch'

接着看fetchIdsByType()调取是fetch

export function fetchIdsByType (type) {
  return fetch(`${type}stories`)
}

接着往下看,fetch是进行网络接口请求,weex中通过stream提供网络访问共鞥,通过stream.fetch获取,这边我们发现fetch函数返回时一个Promise对象,关于Promise是es6,大家可以自己查阅下,这里不进行阐述。

export function fetch (path) {
  //异步请求
  return new Promise((resolve, reject) => {
    stream.fetch({
      //请求方式 
      method: 'GET',
      //请求地址
      url: `${baseURL}/${path}.json`,
      //数据类型
      type: 'json'
    }, (response) => {
      if (response.status == 200) {
        //请求成功 进行成功回调
        resolve(response.data)
      }
      else {
      //请求失败 进行失败回调
        reject(response)
      }
    }, () => {})
  })
}

我们再回过头看下之前获取列表请求的方法

export function FETCH_LIST_DATA ({ commit, dispatch, state }, { type }) {
  commit('SET_ACTIVE_TYPE', { type })
  return fetchIdsByType(type)
    .then(ids => commit('SET_LIST', { type, ids }))//请求成功
    .then(() => dispatch('ENSURE_ACTIVE_ITEMS'))//请求失败
}

第4行是请求成功回调方法,调取multation的SET_LIST方法并进行数据列表lists赋值;
第5行请求失败调取actions的ENSURE_ACTIVE_ITEMS方法;
我们接着看commit调取mutations的SET_LIST类型函数

export function SET_LIST (state, { type, ids }) {
  state.lists[type] = ids
}

在SET_LIST函数中对store中state的lists进行赋值;

UI上填充数据

在StoriesView.vue文件

 <cell class="story-cell" v-for="story in stories" :key="story.id" append="tree">
        <!--:story="story"  将数据story传参到story组件中-->
        <story :story="story"></story>
      </cell>

stories数据调取是script模块函数

 computed: {
      stories () {
        return this.$store.getters.activeItems
      }
    }

this.$store.getters.activeItems调取是store里面的getters模块的activeItems函数:

activeItems (state, getters) {
      return getters.activeIds.map(id => state.items[id]).filter(_ => _)
    }

其实调取函数activeIds

activeIds (state) {
      const { activeType, lists, counts } = state
      return activeType ? lists[activeType].slice(0, counts[activeType]) : []
    }

activeIds 主要做的事过滤store的state模块的lists数据,返回数据是activeType类型文章的lists数据。

三、个人见解

    1、如果只是单纯只是开发单界面,不用考虑工程里面嵌入router和vuex,毕竟只是单个界面不存在路由跳转复杂逻辑和大量的状态管理;
    2、如果开发app项目大部分界面使用weex,那么可以优先考虑嵌入router和vuex,实际项目会很多组件需要维护state状态维护;
    3、开发中的es6语法糖对于移动端开发者,在遇到时候再去看相应的资料,不建议一头扎进es6语法;
    4、项目中的vuex和router知识很重要;
    5、尽管weex-hackernews的没有进行store没有进行模块划分,实际项目建议根据项目需求进行划分。

wee官网                    http://weex.apache.org/cn/
vue官网                     https://cn.vuejs.org
vue-router官网          https://router.vuejs.org/zh-cn/
Vuex官网                   https://vuex.vuejs.org/zh-cn/
大灰狼的小绵羊哥哥的vue-router 60分钟快速入门          
http://blog.csdn.net/sinat_17775997/article/details/52549123

Logo

前往低代码交流专区

更多推荐