97

「大前端」Weex在达人店的一年实践

 6 years ago
source link: https://juejin.im/post/5a2a730cf265da431f4afd35
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Weex在达人店的一年实践

本文来自尚妆移动端团队路飞

发表于尚妆github博客,欢迎订阅!

尚妆达人店接入weex也一年的时间了,在此期间,也陆陆续续出了一些文章:

「Android」 详细全面的基于vue2.0Weex接入过程(Android视角)

「前端」weex页面传参

「大前端」weex里native主动发送事件到JS的方案实现

weex 三端实现Pager 组件(ViewPager) - 本仁笔记

记录团队weex实践过程中需要特殊注意的点

这里就详细地做一个总结,希望可以给大家带来一些参考。我们团队也比较小,App的量级也不大,很多做得不够好的地方,还希望大神不吝赐教。

一、什么是Weex

Weex 是一套简单易用的跨平台开发方案,能以 web 的开发体验构建高性能、可扩展的 native 应用,为了做到这些,Weex 与 Vue 合作,使用 Vue 作为上层框架,并遵循 W3C 标准实现了统一的 JSEngine 和 DOM API,这样一来,你甚至可以使用其他框架驱动 Weex,打造三端一致的 native 应用。

前言引用了Weex官网的定义,我们在实践的过程中也实际地体会到了这些。以下是提炼出的几个关键字:

image

还未接触过weex的同学,如果想先看一下效果,可以访问 Weex 提供的 在线Playground,进行编辑和浏览,App端下载playgroundplayground进行扫码浏览效果。

image

可以看到,Weex可以通过自己设计的DSL,用vue像写 web 页面一样写一个 app 的页面,整个页面书写分成了3段,templatestylescript,借鉴了成熟的MVVM的思想。

后面会讲到,理论上也可以横向支持采用React、angular等框架来书写页面。阿里开源的Rax,就是基于React的标准,支持在Weex渲染,具体可以看知乎上一个问答如何看待阿里开源的Rax框架?

而Playground集成了Weex SDK,扫码后,得到了编译好的JS Bundle,然后通过JS Framework层解析,输出Json格式的Visual Dom,然后通过JS-Native Bridge 来渲染成Native界面,也通过Bridge来进行Js-Native的事件传递。如下是官网给出的架构图:

image

通过断点调试可以看到,JSFramework传给SDK的渲染指令是这样子,SDK 再根据不同的type和参数,渲染成对应的Native组件。

image

传统的App,Native UI 是可以直接获取 Device Power的,而Weex App里,Native UI 和 Device Power之间通过JavaScript来连接,如图所示(图来自weex官网):

image

在开始接入之前,关于Weex的页面结构,需要了解一下,具体可以查看Weex官网的Weex页面结构。为了阅读方便,下面直接引用:

Weex 页面结构

界面展示、逻辑处理、设备能力使用、生命周期管理等部分。

Dom模型

Weex 页面通过类似 HTML DOM 的方式管理界面,首先页面会被分解为一个 DOM 树,,每个 DOM 结点都代表了一个相对独立的 native 视图的单元。然后不同的视图单元之间通过树形结构组合在了一起,构成一个完整的页面。

Weex 支持文字text、图片image、视频video等内容型组件,也支持 div、list、scroller 等容器型组件,还包括 slider、input、textarea、switch 等多种特殊的组件。Weex 的界面就是由这些组件以 DOM 树的方式构建出来的。

Weex 页面中的组件会按照一定的布局规范来进行排布,我们这里提供了 CSS 中的盒模型、flexbox 和 绝对/相对/固定/吸附布局这三大块布局模型。

Weex 提供了非常丰富的系统功能 API,包括弹出存储、网络、导航、弹对话框和 toast 等,开发者可以在 Weex 页面通过获取一个 native module 的方式引入并调用这些客户端功能 API。

每个 Weex 页面都有其自身的生命周期,页面从开始被创建到最后被销毁,会经历到整个过程。这是通过对 Weex 页面的创建和销毁,在路由中通过 SDK 自行定义并实现的。

Weex的扩展性很好,可以对网络、图片、存储、UT、组件、接口等根据自身App和业务需求进行扩展,即使weex提供的组件有问题,也都可以直接重写替换。

image

对于一个新技术的接入,我们首先会去考虑这个技术的优缺点,能给团队和业务带来什么效益;然后考虑接入的成本,包括团队成员的学习成本,对项目的修改成本,时间成本;开发体验,性能监控,容灾处理等。 在考虑完这些之后,OK,我们开始决定接入Weex。

二、达人店接入Weex

达人店目前是一个量级比较小的应用,在一年时间里,目前有46个页面。目前整体都比较稳定,后续所有页面也都会采用weex进行开发。

image

因为Weex给我们带来的效益是显而易见的:

  • 3人/日 -> 1人/日
  • 大程度摆脱App更新限制
  • Native 体验

在接入的过程中,我们在各方面做了很多事情,包括脚手架、配置下发、跳转规则、相对地址、预加载、降级、错误监控、建立组件库、页面传参等等。下面详细介绍一下这个过程,如果您有更好的方法,非常欢迎进行讨论交流。

(一) 前端

首先要建立Weex项目,这个可以看做是一个前端的项目,Weex也提供了脚手架工具。

weex 推荐的脚手架全家桶:

  • weex-toolkit:用来初始化项目,编译,运行,debug所有工具。
  • weexpack:用来打包JSBundle的,实际也是对Webpack的封装。
  • playground:一个上架的App,这个可以用来通过扫码实时在手机上显示出实际的页面。
  • code snippets:这个是一个在线的playground。
  • weex devtools:就是为weex前端和native开发工程师服务的一款调试工具。
  • weex-loader:Webpack 的一个加载器,针对 Android 和 iOS 平台,用于编译 .vue 格式的单文件组件

达人店没有使用weex提供的脚手架,而是我们前端同学定义了适合我们业务的项目结构,以下是达人店的Weex项目结构的一部分,每个页面有一个文件夹,包含了html,js,vue: html文件:接入weex 的h5页面 js文件:webpack编译的入口文件 vue文件:weex的编辑页面

以下是开发环境的示例,所以引入的js都没有版本号,正式环境的path里会有版本号

HTML示例 其中,/dist/weex.js 引入weex-vue-render,进行了扩展,包括注册module,注册新的自定义组件。weex-vue-render可以理解为weex在H5的SDK。详情见 HTML扩展

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
    <script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
  </head>
  <body>
    <div id="weex"></div>
	<!-- entry -->
	<script src="//assets.showjoy.net/joyf2e/vendor/weex-extend/dist/weex.js" type="text/javascript"></script>
	<script src="./register-weex.min.js" type="text/javascript"></script>
  </body>
</html>
复制代码

Js示例 #weex 就是对应html里 <div id="weex"></div>,vue渲染后会挂在这个div上。

import weexComponent from './register-weex.vue';
weexComponent.el = '#weex';
export default new Vue(weexComponent);
复制代码

Vue示例

<template>
  <div class="wrapper">
  <div>
</template>
<style scoped>
  .wrapper {
    background-color: #fff;
    flex: 1;
  }
</style>
<script>
</script>
复制代码

构建的时候定义了两套webpackConfig,分别用于编译给h5和Native的JS。之所以需要分开编译,是出于weex的要求,下文来自Weex官网,我们在Jenkins上实现了远程构建。

编译环境的差异

在 Weex 中使用 Vue.js ,你所需要关注的运行平台除了 Web 之外还有 Android 和 iOS ,在开发和编译环境上还有一些不同点。针对 Web 和原生平台,将 Vue 项目源文件编译成目标文件,有两种不同的方式:

  • 针对 Web 平台,和普通 Vue 2.X 项目一样,可以使用任意官方推荐的方式编译源文件,如 Webpack + vue-loader 或者 Browserify + vueify 。
  • 针对 Android 和 iOS 平台,我们提供了 weex-loader 工具支持编译 .vue 格式的单文件组件;也就是说,目前只能使用 Webpack + weex-loader 来生成原生端可用的 js bundle。

(二) Native 接入

请直接参考官网集成 Weex 到已有应用,SDK的依赖,初始化,渲染,都已说明。

说到底,最后的渲染结果都是返回一个View,理论上根据业务需求,可以将view放置在页面的任何地方。我们达人店,都是整个页面的形式来引入weex。

在Android方面,我们把weex的接入放入了自定义的WeexFragment。另外,新建WeexActivity,引用WeexFragment。这样使用起来更灵活。

在iOS方面,我们把weex的接入放入了自定义的WeexViewController。

(三)跳转规则

Native 渲染weex页面的时候,需要传入构建出来的js bundle,即一个js文件。但是,不管是Native的日常写法还是前端的惯常用法,都不会直接跳转到一个js文件。所以,考虑到符合前端的日常写法,跳转时,统一跳转到url,如下图:

不管是weex,native,webview里的跳转都是url,然后再根据一定的规则进行match,根据match结果来决定是用weex、native还是webview来打开。

  • 要做到weex,native,webview里的跳转都是url,这里需要做两点:
    • 1、跳转需要调用统一的openUrl,weex里的a标签href直接可以写目标url,然后在Native端对a标签的跳转进行拦截;
    • 2、webview 里的跳转进行拦截,每个url都要进行规则匹配
  • 定义规则,App内置一份,并可以动态下发
    • 1、url 和 原先 Native 页面的对应关系,page可以根据原先App里的Router设计来定义。
    • 2、url 和 weex js的对应关系, hideTitleBar:是否隐藏native的titlebar; v:支持最低App版本,不支持就降级; page: 页面名称,作为本地预加载的文件名; h5: h5的url; url: js的路径; md5: js文件的md5,用于完整性校验

url 和 Native 页面的对应关系示例

		 [			    {			        "page":"chat",			        "url":"(.*)//shop.m.showjoy.net/shop/chat\?type=1",			        "v":"1.7.0"			    },			    {			        "page":"main",			        "url":"(.*)//shop.m.showjoy.net/shop/seller_home",			        "v":"1.12.0"			    }	]
复制代码

url和weex页面对应关系示例

[	{	"hideTitleBar": "",	"v": "1.7.0",	"page": "order",	"h5": "http://shop.m.showjoy.com/u/trade.html",	"url": "http://cdn1.showjoy.com/assets/f2e/showjoy-assets/shop-weex-m/0.8.1/order-list-weex/order-list-weex.weex.min.js",	"md5": "8b3268ef136291f2e9b8bd776e625c6b"	},	{	"hideTitleBar": "",	"v": "1.7.0",	"page": "shoporder",	"h5": "http://shop.m.showjoy.com/user/tradePage",	"url": "http://cdn1.showjoy.com/assets/f2e/showjoy-assets/shop-weex-m/0.1.1/shop-order-weex/shop-order-weex.weex.min.js",	"md5": "ca818a24588509bfe083cd4b99855841"	}]
复制代码

(四)配置平台

针对跳转规则的配置,我们做了自己的配置平台,针对全量、预发、线下提供不同的配置。参数:

  • appType:1代表android 2代表iOS
  • preTest: true 代表预发 false 代表全量
  • appVersion:App的版本号

平台会根据三个参数,下发当前App支持渲染的js页面配置。

(五)支持相对地址

按照平常前端的写法,跳转以及a标签写的基本都是相对地址,这样对于线下、线上环境都不用做特别的处理。如下:

开始介入weex的时候,大概版本是0.8左右,那时候默认还不支持相对地址,而我们就已经开始自己做了。在weex sdk 0.9.4开始 默认支持了相对地址,但是通过测试和源代码查看,它取的host是js bundle的host,如图:

而我们把js bundle放在了cdn,日常页面的域名是shop.m.showjoy.com,两者不一致,所以在Native端,我们重写了URIAdapter(Android)和WXURLRewriteProtocol(iOS),对url进行了处理,如果是相对地址,加上日常h5页面的host,请求也是一样。如此就支持了相对地址。 Andoird

//这里还可以配置其他的adapter,比如image,storage等
WXSDKEngine.initialize(application,
                new InitConfig.Builder()
                        .setURIAdapter(new SHCustomURIAdapter())
                        .build());
复制代码

iOS

[WXSDKEngine registerHandler:[WXSJNetworkDefaultlmpl new] withProtocol:@protocol(WXURLRewriteProtocol)];
复制代码

实现的时候,重写rewrite接口,我们会根据线下、线上、预发等环境配置不一样的host,另外还会支持Native的协议,如:sms://, weixin://dl/privacy

PS: A 标签的跳转,Native SDK的实现是调用Module“event”的openURL接口。可是默认没有注册“event”的Module,所以需要自己注册event,或者自己重新实现 a标签。 sdk里对a标签跳转的处理

自定义event module.

(六)预加载方案

如图,是在本地开发时抓的包,加载的js bundle 虽然也不大,duration也很短。但是为了让速度更进一步,我们还是做了预加载方案。

方案设计如下:

  • 1)每次更新完配置文件,遍历,check pagename.js文件的md5
  • 2)如果本地存在md5一致的文件,就跳过,否则下载
  • 3)下载完成后,保存格式为pagename.js,已存在则覆盖,校验md5来保证文件的完整性:
    • 相同的话,记录文件的最后修改时间;
    • 不同的话,删除已下载文件,重新下载,重复校验流程。
  • 4)每次打开指定页面的时候:
    • 先检查本地是否有对应page文件
      • 如果不存在,则直接使用配置里的remote url
      • 如果存在,则校验记录的修改时间是否与该文件的最后修改时间是否一致(这么做,是为了防篡改;不直接计算md5来校验,是考虑到md5的计算有时间消耗)
        • 一致就加载
        • 不一致就用配置里的remote url

(七)Native-JS通信

1、JS 调用Native
  • Weex 提供了Module扩展接口,开发者可以自己注册Module,Module里定义接口;
2、Native调用JS
  • Module接口可以设置Callback,接口实现处理完后,可以直接调用Callback,回调JS.

  • WXSDKInstance.fireEvent 是元素级别的,fireEvent 是instance的成员函数,需要传递elementRef。

  • WXSDKInstance.fireGlobalEventCallback 是页面级别的,需要传递instanceID

(八)错误监控

*Native 端可以通过接口 IWXRenderListener 中的 onException 方法进行处理,这里包括render error,js exception,network error等。

  • Weex层,自定义loge接口来实现错误的监控

(九)页面传参

关于页面传参,我们团队的南洋同学写过一篇文章(Weex页面传参)[https://juejin.cn/post/6844903491299704840],为了方便阅读,这里再讲述一遍。

1、正向传参:x.com/a.html 跳转到 x.com/b.html?age=12 Native 渲染的时候,除了传入JS Bundle,还有options参数,我们把url后面的参数都存入options,然后传到weex页面。

[_instance renderWithURL:[NSURL URLWithString:mstrURL] options:[self SHWeexOptionsWithH5URL:mstrH5URL withURL:mstrURL] data:nil];
复制代码

这个参数,在书写weex时,可以通过weex.config.age获取。

为了获取参数的统一性,H5页面也一样,打开一个url时,首先获取url后面的参数,存入window.weex.config。

for (let key in urlParamObj) {
  window.weex.config[key] = encodeURIComponent(urlParamObj[key]);
}
复制代码

2、反向传参:x.com/b.html 回退到 x.com/a.html,带回参数age=2 这个是为了实现类似Android里 onActivityResult的功能,可以把参数传回给上个页面。而实现这样的功能,iOS Native的实现也只要加个Delegate就可以了。

在weex要实现这个效果,本身没有提供直接可以使用的方法,下面是我们目前采取的方案。

  • 首先自定义定义Module,增加setResult接口,然后再weex调用,参数是k-v的形式。接口的实现,就是把数据先存在本地;

  • 回到上个页面,resume/willappear时候,获取存储的k-v,并通过fireGlobalEventCallback把数据传递到weex页面。,并且remove数据。

  • 在weex页面进行监听,并处理

(十) 降级方案

所谓降级,就是当前新页面渲染失败,或者当前App版本不够新,无法支持新页面,故会访问h5页面。这里我们区分了两种情况:

  • 1、渲染失败: 一致跳转到h5页面
  • 2、版本控制:
    • 新增的页面:无法支持新页面的App版本就降级访问h5页面
    • 老页面的修改:无法支持新页面的App版本会访问老页面

(十一)屏幕适配

屏幕适配一直是移动端开发不可避开的话题。在Weex的世界里,定义了一个默认屏幕尺寸,用来适配iOS,Android各种不同大小的屏幕。weex框架在底层做了针对不同屏幕的适配工作,具体计算公式为 实际高宽 = 代码高宽 * (屏幕宽度 / 750)

目前我们设计给的视觉稿是375的,我们开发的时候只要拿到值x2,就可以了。 其中有一种普遍会遇到需要的计算的地方,这里详细讲一下。

使用List和scroll的时候,高度是需要设置的,而这个高度需要根据不同页面进行计算,以上图为例,首先想到的是: list高度 = screen高度 - titlebarHeight

weex可以通过$getConfig().env.deviceHeight$getConfig().env.deviceWidth的形式来获取手机屏幕的高度 但是其实这样是不准确的,因为Android Native的总高度,事实上是可供显示的全屏高度,而不一定是物理屏幕的高度,因为有状态栏,虚拟按键栏,Smartbar等等安卓碎片化引入的额外显示元素,实际全屏高度很有可能小于物理屏幕高度。 所以真正的容器高度,需要由外部传入,

List实际高度 = ContainnerHeight - titleBar的高度字面量 * 转换比例ratio 转化比例ratio = this.$getConfig().env.deviceWidth / 750

ps: 外部传入的ContainnerHeight通过Module的接口传入

list的字面量高度 = list实际高度 / 转换比例ratio = ContainnerHeight / ratio - titleBar的高度字面量

另外,weex也提供this.$getConfig().env.scale,如有需要可以利用它来计算dp2px。

三、我们遇到的一些问题和解决方案

1)Android 的weex sdk 0.13.1,input组件初始值是空时,粘贴的时候无法触发事件@input 设置初始值,点击时,如果初始值与placeholder一致,就清空

2)在iOS9.x系统中文本被截断 在iOS9.x系统中不支持line-height,被强行绘制,存在兼容性问题,暂时不要使用font-size和line-height相同大小

3)class 的动态绑定 vue的写法 :class={'header': true} weex的写法 :class=“[true ? 'header' : '']"

4)animation动画在iOS 8及以下的H5页面失效 对于webkit不兼容的css样式(transform)进行兼容

5)scroller横向滚动时iOS设备元素无法横向排列 需要给scroller设置样式 flex-deriction: row,这样可以确保三端显示一致。

6)Js Date 转换时间,Android差8小时 dateConfigTimeZone(timeValue, offset) { const date = new Date(timeValue); // UTC时间 (1970-1-1至今毫秒数 + 本地时间与GMT分钟差) const utc = date.getTime() + (date.getTimezoneOffset() * 60 * 1000); // 返回 (UTC时间 + 时区差) return new Date(utc + (60 * 60 * 1000 * offset)); }

四、我们还在做的事情

(一)weex组件库

一年的实践,我们也积累了一些基础组件和业务组件,如图,有description、import、example、preview、qrcode等。

image

看下 spon-ui 组件库项目的目录结构。

|- spon-ui
||-- build
||-- docs
||-- examples
||-- packages
|||--- weex-field
||||---- index.js
||||---- field.vue
||||---- example.vue
||||---- readme.md
||||---- package.json
||-- src
复制代码
  • build 中存放一些脚本执行文件,用于工程的调试、发布。
  • docs 中存放文档调试的脚本,生成一个文档调试服务器。
  • examples 中存放组件调试的脚本,生成一个组件调试服务器。(不存放组件例子)
  • packages 存放真实组件,以及组件的文档和例子。
  • src 存放组件可以使用的公共方法。

详情请查看我们的前端同学南洋写「大前端」尚妆达人店 UI 组件化 工程实践

(二) 其他

  • Cookie 支持
  • HttpDNS 接入
  • 图片支持裁剪、webp
  • 性能监控,正在做
  • 增量更新,正在做

以上就是我们这一年的总结,希望能给大家带来参考。欢迎讨论交流文中的不足。

感谢团队所有成员,以上是我们一起努力的结果。

@嘉文,资深iOS,github博客

@黎鹤,资深iOS,github

@路远,资深Android,github博客

@米奇,前端女神,欢迎关注微博

@南洋,前端大神,欢迎关注微博 ,技术文章产出高,

@路飞,移动端负责人,github博客

感谢以下大神文章提供的帮助:(看了很多文章,如果没有加了,麻烦告知一声)

Weex官网

Weex github

Rax官网

网易严选App感受Weex开发

由FlexBox算法强力驱动的Weex布局引擎

Weex 事件传递的那些事儿

Weex 中别具匠心的 JS Framework

地球上最全的weex踩坑攻略-出自大量实践与沉淀


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK