weex官方weex-hackernews源码解读
weex官方weex-hackernewsweex-hackernewsWeex官方基于Weex和Vue开发了一个的完整项目,在项目中使用了Vuex和vue-router,能够实现同一份代码,在 iOS、Android、Web 下都能完整地工作。weex-hackernews的项目地址。运行weex-hackernews如果搭建开发环境没有搭建的,可以前往Weex官方按照开发文档流
一、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
更多推荐
所有评论(0)