> 这是一篇有故事的文章 --- 来自一个weex在生产环境中相爱相杀的小码畜..故事一: Build
虽然
weex
的口号是
一次撰写 多端运行
, 但其实
build
环节是有差异的,
native
端构建需要使用
weex-loader
, 而
web
端则是使用
vue-loader
,除此以外还有不少差异点, 所以
webpack
需要两套配置.
最佳实践
使用
webpack
生成两套
bundle
,一套是基于
vue-router
的
web spa
, 另一套是
native
端的多入口的
bundlejs
首先假设我们在
src/views
下开发了一堆页面
build web配置
web端的入口文件有
render.js
import weexVueRenderer from 'weex-vue-render'Vue.use(weexVueRenderer)
main.js
import App from './App.vue'import VueRouter from 'vue-router'import routes from './routes'Vue.use(VueRouter)
App.vue
<template>
webpack.prod.conf.js
入口
const webConfig = merge(getConfig('vue'), {
build native配置
native端的打包流程其实就是将
src/views
下的每个
.vue
文件导出为一个个单独的
vue
实例, 写一个
node
脚本即可以实现
// build-entry.js
// build-entry.js require('shelljs/global') const path = require('path') const fs = require('fs-extra') const srcPath = path.resolve(__dirname, '../src/views') // 每个.vue页面 const entryPath = path.resolve(__dirname, '../entry/') // 存放入口文件的文件夹 const FILE_TYPE = '.vue' const getEntryFileContent = path => { return `// 入口文件 import App from '${path}${FILE_TYPE}' /* eslint-disable no-new */ new Vue({ el: '#root', render: h => h(App) }) ` } // 导出方法 module.exports = _ => { // 删除原目录 rm('-rf', entryPath) // 写入每个文件的入口文件 fs.readdirSync(srcPath).forEach(file => { const fullpath = path.resolve(srcPath, file) const extname = path.extname(fullpath) const name = path.basename(file, extname) if (fs.statSync(fullpath).isFile() && extname === FILE_TYPE) { //写入vue渲染实例 fs.outputFileSync(path.resolve(entryPath, name + '.js'), getEntryFileContent('../src/views/' + name)) } }) const entry = {} // 放入多个entry fs.readdirSync(entryPath).forEach(file => { const name = path.basename(file, path.extname(path.resolve(entryPath, file))) entry[name] = path.resolve(entryPath, name + '.js') }) return entry }
webpack.build.conf.js
中生成并打包多入口
const buildEntry = require('./build_entry') // .. // weex配置 const weexConfig = merge(getConfig('weex'), { entry: buildEntry(), // 写入多入口 output: { path: path.resolve(distPath, './weex'), filename: 'js/[name].js' // weex环境无需使用hash名字 }, module: { rules: [ { test: /\.vue$/, loader: 'weex-loader' } ] } }) module.exports = [webConfig, weexConfig]
最终效果
故事二: 使用预处理器
在
vue
单文件中, 我们可以通过在
vue-loader
中配置预处理器, 代码如下
{ test: /\.vue$/, loader: 'vue-loader', options: { loaders: { scss: 'vue-style-loader!css-loader!sass-loader', // <style> sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax' // <style> } } }
而
weex
在native环境下其实将
css
处理成
json
加载到模块中, 所以...
使用
vue-loader
配置的预处理器在web环境下正常显示, 在
native
中是无效的
native环境下不存在全局样式, 在js文件中
import 'index.css'
也是无效的
解决问题一
研究
weex-loader
源码后发现在
.vue
中是无需显示配置
loader
的, 只需要指定
<style>
并且安装
stylus stylus-loader
即可,
weex-loader
会根据
lang
去寻找对应的
loader
. 但因为
scss
使用
sass-loader
, 会报出
scss-loader not found
, 但因为
sass
默认会解析
scss
语法, 所以直接设置
lang="sass"
是可以写
scss
语法的, 但是
ide
就没有语法高亮了. 可以使用如下的写法
<style> @import './index.scss' </style>
语法高亮, 完美!
解决问题二
虽然没有全局样式的概念, 但是支持单独
import
样式文件
<style lang="sass">
故事三: 样式差异
这方面官方文档已经有比较详细的描述, 但还是有几点值得注意的
简写
weex
中的样式不支持简写, 所有类似
margin: 0 0 10px 10px
的都是不支持的
背景色
android
下的view是有白色的默认颜色的, 而iOS如果不设置是没有默认颜色的, 这点需要注意
浮点数误差
weex
默认使用
750px * 1334px
作为适配尺寸, 实际渲染时由于浮点数的误差可能会存在几
px
的误差, 出现细线等样式问题, 可以通过加减几个
px
来调试
嵌套写法
即使使用了预处理器,
css
嵌套的写法也是会导致样式失效的
故事四: 页面跳转
weex
下的页面跳转有三种形式
native -> weex
:
weex
页面需要一个控制器作为容器, 此时就是
native
间的跳转
weex -> native
: 需要通过module形式通过发送事件到native来实现跳转
weex -> weex
: 使用navigator模块, 假设两个
weex
页面分别为
a.js, b.js
, 可以定义
mixin
方法
function isWeex () { return process.env.COMPILE_ENV === 'weex' // 需要在webpack中自定义
这样就组件里使用
this.push(url), this.pop()
来跳转
跳转配置
iOS下页面跳转无需配置, 而
android
是需要的, 使用
weexpack platform add android
生成的项目是已配置的, 但官方的文档里并没有对于已存在的应用如何接入进行说明
其实
android
中是通过
intent-filter
来拦截跳转的
<activity
然后我们新建一个
WXPageActivity
来代理所有
weex
页面的渲染, 核心的代码如下
[@Override](/user/Override) protected void onCreate(Bundle saveInstanceState) { // ...
顺便说下...
weex
官方没有提供可定制的
nav
组件真的是很不方便..经常需要通过
module
桥接
native
来实现跳转需求
来自@荔枝我大哥 的补充
安卓和苹果方面可以在原生代码接管`navigator`这个模块,安卓方面只需要实现`IActivityNavBarSetter`,苹果方面好像是`WXNavigatorProtocol`,然后在app启动初始化weex时注册即可。
故事五: 页面间数据传递
native -> weex
: 可以在
native
端调用
render
时传入的
option
中自定义字段, 例如
NSDictary *option = @{@"params": @{}}
, 在
weex
中使用
weex.config.params
取出数据
weex -> weex
: 使用storage
weex -> native
: 使用自定义module
故事六: 图片加载
官网有提到如何加载网络图片 但是加载本地图片的行为对于三端肯定是不一致的, 也就意味着我们得给
native
重新改一遍引用图片的路径再打包...
但是当然是有解决办法的啦
Step 1
webpack
设置将图片资源单独打包, 这个很easy, 此时
bundleJs
访问的图片路径就变成了
/images/..
{
Step 2 那么现在我们将同级目录下的js文件夹与images文件夹放入
native
中, iOS中一般放入
mainBundle
, Android一般放入
src/main/assets
, 接下来只要在
imgloader
接口中扩展替换本地资源路径的代码就ok了
iOS
代码如下:
- (id<WXImageOperationProtocol>)downloadImageWithURL:(NSString *)url imageFrame:(CGRect)imageFrame userInfo:(NSDictionary *)options completed:(void (^)(UIImage *, NSError *, BOOL))completedBlock{ if ([url hasPrefix:@"//"]) {
Android
代码如下:
[@Override](/user/Override) public void setImage(final String url, final ImageView view,
故事七: 生产环境的实践
增量更新
方案一
可以使用google-diff-match-patch来实现, google-diff-match-patch拥有许多语言版本的实现, 思路如下:
服务器端构建一套管理前端
bundlejs
的系统, 提供查询
bundlejs
版本与下载的api
客户端第一次访问
weex
页面时去服务端下载
bundlejs
文件
每次客户端初始化时静默访问服务器判断是否需要更新, 若需更新, 服务器端
diff
两个版本的差异, 并返回
diff
,
native
端使用
patch api
生成新版本的
bundlejs
方案二
来自 @荔枝我大哥的补充
我们所有的jsBundle全部加载的线上文件,通过http头信息设置`E-Tag`结合`cache-control`来实现缓存策略,最终效果就是,A.vue -> A.js, app第一次加载A.js是从网络下载下来并且保存到本地,app第二次加载A.js是直接加载的保存到本地的 A.js文件,线上A.vue被修改,A.vue -> A.js, app第三次加载A.js时根据缓存策略会知道线上A.js 已经和本地A.js 有差异,于是重新下载A.js到本地并加载. (整个流程通过http缓存策略来实现,无需多余编码,参考https://developers.google.cn/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=zh-cn)
还可以参考很多ReactNative的成熟方案, 本质上都是js的热更新
降级处理
一般情况下, 我们会同时部署一套
web
端界面, 若线上环境的
weex
页面出现bug, 则使用webview加载
web
版, 推荐依赖服务端api来控制降级的切换
总结
weex
的优势: 依托于
vue
, 上手简单. 可以满足以
vue
为技术主导的公司给
native
双端提供简单/少底层交互/热更新需求的页面的需求
weex
的劣势: 在
native
端调整样式是我心中永远的痛.. 以及众所周知的生态问题, 维护组没有花太多精力解答社区问题, 官方文档错误太多, 导致我在看的时候就顺手提了几个PR(逃
对于文章中提到的没提到的问题, 欢迎来和笔者讨论, 或者参考我的weex-start-kit, 当然点个star也是极好的