weex官方weex-hackernews源码解读

来源:互联网 发布:淘宝客域名备案 编辑:程序博客网 时间:2024/06/07 14:29

一、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.jsnpm run build# 监听模式的 npm run buildnpm run dev

拷贝bundle文件:

# 将生成的 bundle 文件拷贝到 Android 项目的资源目录npm run copy:android# 将生成的 bundle 文件拷贝到 iOS 项目的资源目录npm run copy:ios# run both copy:andriod and copy:iosnpm 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 factoryfunction 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的storeimport { 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