weex-vue-render代码浅析

最近在做一些Weex web端的东西,不可避免的和weex-vue-render打起了交道,为了能更好的理解web端的构建,所以花了一点时间走读了一遍weex-vue-render的源码,这里做一个记录,以防遗忘。

weex-vue-render是什么

这是它的github地址

我们知道Weex在构建的时候会打包出一个js给客户端进行加载,而如果我们需要在web端也使用同一套代码进行渲染的话,还需要一个web端用的js,并且通过weex-vue-render去渲染。所以一句话概括,weex-vue-render就是用来渲染web端Weex的工具。

源码分析

首先我们来看工程下的入口文件:packages/src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import weex from '../../../src'

import components from '../../../src/components'
import modules from '../../../src/modules'
import directives from '../../../src/directives'

const preInit = weex.init

weex.init = function () {
preInit.apply(weex, arguments)
const plugins = components.concat(modules)

plugins.forEach(function (plugin) {
weex.install(plugin)
})

for (const k in directives) {
weex.install(directives[k])
}
}

if (global.Vue) {
weex.init(global.Vue)
}

export default weex

首先组装了一个数组,里面是components和modules,并且调用了weex的install方法去注册,之后调用了init方法初始化,最终将其export出去。

从这里我们就知道,为什么在加载了这个js之后,我们就可以获取一个weex对象,这个weex对象就是这里export出去的。

下面我们来看看weex.install都做了什么吧~

1
2
3
install (module) {
module.init(this)
}

而components和modules这个数组都有些什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default [
geolocation,
storage,
stream,
clipboard,
eventModule,
modal,
websocket,
animation,
dom,
globalEvent,
navigatorModule,
webview,
meta
]
1
2
3
4
5
6
7
8
9
10
11
12
13
export default [
// a,
// div,
// image,
input,
_switch,
scrollable,
slider,
// text,
textarea,
video,
web
]

可以看到,这些其实就和我们客户端所注册的是一样的。我们挑一个来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function getTextarea (weex) {
const { extractComponentStyle } = weex
const { inputCommon } = weex.mixins
const { extend, mapFormEvents } = weex.utils

return {
name: 'weex-textarea',
mixins: [inputCommon],
props: {
value: String,
placeholder: String,
disabled: {
type: [String, Boolean],
default: false
},
autofocus: {
type: [String, Boolean],
default: false
},
rows: {
type: [String, Number],
default: 2
},
returnKeyType: String
},

render (createElement) {
/* istanbul ignore next */
// if (process.env.NODE_ENV === 'development') {
// validateStyles('textarea', this.$vnode.data && this.$vnode.data.staticStyle)
// }
const events = extend(mapFormEvents(this))
return createElement('html:textarea', {
attrs: {
'weex-type': 'textarea',
value: this.value,
disabled: (this.disabled !== 'false' && this.disabled !== false),
autofocus: (this.autofocus !== 'false' && this.autofocus !== false),
placeholder: this.placeholder,
rows: this.rows,
'return-key-type': this.returnKeyType
},
domProps: {
value: this.value
},
on: this.createKeyboardEvent(events),
staticClass: 'weex-textarea weex-el',
staticStyle: extractComponentStyle(this)
})
}
}
}

export default {
init (weex) {
weex.registerComponent('textarea', getTextarea(weex))
}
}

这是一个component,它的init方法,其实就是调用了weex的registerComponent将其注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
registerComponent (name, component) {
if (!this.__vue__) {
return console.log('[Vue Render] Vue is not found. Please import Vue.js before register a component.')
}
this._components[name] = 0
if (component._css) {
const css = component._css.replace(/\b[+-]?[\d.]+rem;?\b/g, function (m) {
return parseFloat(m) * 75 * weex.config.env.scale + 'px'
})
utils.appendCss(css, `weex-cmp-${name}`)
delete component._css
}
this.__vue__.component(name, component)
}

而registerComponent方法,也就是调用了vue的component方法了~

说完了install,我们再来看一下init方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function init (Vue/*, options = {}*/) {
if (_inited) { return }
_inited = true

setVue(Vue)

Vue.prototype.$getConfig = () => {
console.warn('[Vue Render] "this.$getConfig" is deprecated, please use "weex.config" instead.')
return weex.config
}

const htmlRegex = /^html:/i
Vue.config.isReservedTag = tag => htmlRegex.test(tag)
Vue.config.parsePlatformTagName = tag => tag.replace(htmlRegex, '')

function isWeexTag (tag) {
return typeof weex._components[tag] !== 'undefined'
}
const oldGetTagNamespace = Vue.config.getTagNamespace
Vue.config.getTagNamespace = function (tag) {
if (isWeexTag(tag)) {
return
}
return oldGetTagNamespace(tag)
}

Vue.mixin(base)
Vue.mixin(event)
Vue.mixin(style)
Vue.mixin(sticky)
}

其中会调用setVue方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
export function setVue (vue) {
if (!vue) {
throw new Error('[Vue Render] Vue not found. Please make sure vue 2.x runtime is imported.')
}
if (global.weex.__vue__) {
return
}
global.weex.__vue__ = vue
weex.install(renderFunctionPlugin)
console.log(`[Vue Render] install Vue ${vue.version}.`)
}

export default weex

可以看到,这里将vue进行了赋值。而这里的weex,其实是src/weex/instance.js。

大家去看这个js,可以看到里面有很多方法,例如registerComponent,registerModule等,也就是上面提到的。

而在这个js中,会去import一个wx-env:

1
import './wx-env'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import '../lib/envd'

import { init as initViewport } from './viewport'
import { extend } from '../utils'

/**
* get WXEnvironment info.
* @param {object} viewportInfo: info about viewport.
* @param {object} envInfo: info parsed from lib.env.
*/

export function initEnv (viewportInfo, envInfo) {
const browserName = envInfo.browser ? envInfo.browser.name : navigator.appName
const browserVersion = envInfo.browser ? envInfo.browser.version.val : null
let osName = envInfo.os.name
if (osName.match(/(iPhone|iPad|iPod)/i)) {
osName = 'iOS'
}
else if (osName.match(/Android/i)) {
osName = 'android'
}
const osVersion = envInfo.os.version.val
const env = {
platform: 'Web',
weexVersion: 'process.env.WEEX_VERSION',
userAgent: navigator.userAgent,
appName: browserName,
appVersion: browserVersion,
osName,
osVersion,
deviceModel: envInfo.os.name || null
}
/**
* viewportInfo: scale, deviceWidth, deviceHeight. dpr
*/

return extend(viewportInfo, env)
}

// const viewportInfo = initViewport()

// 750 by default currently
// const scale = viewportInfo.scale

// const units = {
// REM: 12 * scale,
// VW: viewportInfo.deviceWidth / 100,
// VH: viewportInfo.deviceHeight / 100,
// VMIN: Math.min(viewportInfo.deviceWidth, viewportInfo.deviceHeight) / 100,
// VMAX: Math.max(viewportInfo.deviceWidth, viewportInfo.deviceHeight) / 100,
// CM: 96 / 2.54 * scale,
// MM: 96 / 25.4 * scale,
// Q: 96 / 25.4 / 4 * scale,
// IN: 96 * scale,
// PT: 96 / 72 * scale,
// PC: 96 / 6 * scale,
// PX: scale
// }

// Object.freeze(units)
// Object.freeze(env)

// window.CSS_UNIT = units
window.WXEnvironment = initEnv(initViewport(), window.lib.env)

这个类会调用initEnv方法,而initEnv的入参需要调用initViewport方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
export function init (viewportWidth = width) {
if (!isInited) {
isInited = true

const doc = window.document
if (!doc) {
console.error('[vue-render] window.document is undfined.')
return
}
if (!doc.documentElement) {
console.error('[vue-render] document.documentElement is undfined.')
return
}

dpr = info.dpr = window.devicePixelRatio
screenWidth = doc.documentElement.clientWidth
screenHeight = doc.documentElement.clientHeight

const resetDeviceHeight = function () {
screenHeight = doc.documentElement.clientHeight
const env = window.weex && window.weex.config.env
info.deviceHeight = env.deviceHeight = screenHeight * dpr
}

// set root font for rem.
setRootFont(screenWidth, viewportWidth)
setMetaViewport(viewportWidth)

window.addEventListener('resize', resetDeviceHeight)

/**
* why not to use window.screen.width to get screenWidth ? Because in some
* old webkit browser on android system it get the device pixel width, which
* is the screenWidth multiply by the device pixel ratio.
* e.g. ip6 -> get 375 for virtual screen width.
*/

const scale = screenWidth / viewportWidth
/**
* 1. if set initial/maximum/mimimum-scale some how the page will have a bounce
* effect when user drag the page towards horizontal axis.
* 2. Due to compatibility reasons, not to use viewport meta anymore.
* 3. viewport meta should always be:
* <meta name="viewport"
* content="width=device-width,
* initial-scale=1,
* maximum-scale=1,
* user-scalable=no" />
*/

extend(info, {
scale,
rootValue: viewportWidth / 10,
deviceWidth: screenWidth * dpr,
deviceHeight: screenHeight * dpr
})
}

return info
}

这个方法做的事情其实就是初始化viewport。

1
setRootFont(screenWidth, viewportWidth)

这一段是设置html的font-size。

1
setMetaViewport(viewportWidth)

这一段是设置weex的weex-viewport标签。

通过这些设置,Weex可以做到在web端不同机型的适配。

1
2
3
4
5
6
7
8
9
10
11
12
13
const DEFAULT_VIEWPORT_WIDTH = 750

function setRootFont (width, viewportWidth, force) {
const doc = window.document
const rem = width * 750 / viewportWidth / 10
if (!doc.documentElement) { return }
const rootFontSize = doc.documentElement.style.fontSize
if (!rootFontSize || force) {
doc.documentElement.style.fontSize = rem + 'px'
}
info.rem = rem
info.rootValue = viewportWidth / 10
}

rem这个变量的计算我们代入具体的数字就可以知道,其实就是screenWdith/10(因为viewportWidth默认是750)。

这样呢,我们html的font-size就是screenWidth/10了。

这个我们需要结合文章开头讲到的webpack配置来一起看:

1
2
3
4
5
6
require('postcss-plugin-px2rem')({
// base on 750px standard.
rootValue: 75,
// to leave 1px alone.
minPixelValue: 1.01
})

在配置中,我们使用了postcss-plugin-px2rem插件,并且设置rootValue是75。这样的话,我们在写代码的时候所有的px值都会被除以75并转换成rem,结合刚才的font-size,我们就可以做到屏幕的适配了。

总结

通过上面的源码分析,我们可以得出weex-vue-render做了以下几件事:

  1. export出了一个weex对象(scr/weex/instance)。该对象具有registerComponent,registerModule等方法。
  2. 通过调用weex.install这个方法注册了所有基础的module和component。
  3. 通过设置html的font-size,并且配合postcss-plugin-px2rem插件做到了适配不同的屏幕。