diff --git a/packages/core/src/core/proxy.js b/packages/core/src/core/proxy.js
index dfc8d28576..8bc29353bc 100644
--- a/packages/core/src/core/proxy.js
+++ b/packages/core/src/core/proxy.js
@@ -44,6 +44,7 @@ import {
ONHIDE,
ONRESIZE
} from './innerLifecycle'
+import contextMap from '../vnode/context'
let uid = 0
@@ -129,6 +130,8 @@ export default class MpxProxy {
}
created () {
+ // 缓存上下文,在 destoryed 阶段删除
+ contextMap.set(this.uid, this.target)
if (__mpx_mode__ !== 'web') {
// web中BEFORECREATE钩子通过vue的beforeCreate钩子单独驱动
this.callHook(BEFORECREATE)
@@ -188,6 +191,8 @@ export default class MpxProxy {
}
unmounted () {
+ // 页面/组件销毁清除上下文的缓存
+ contextMap.remove(this.uid)
this.callHook(BEFOREUNMOUNT)
this.scope?.stop()
if (this.update) this.update.active = false
@@ -377,7 +382,10 @@ export default class MpxProxy {
this.doRender(this.processRenderDataWithStrictDiff(renderData))
}
- renderWithData (skipPre) {
+ renderWithData (skipPre, vnode) {
+ if (vnode) {
+ return this.doRenderWithVNode(vnode)
+ }
const renderData = skipPre ? this.renderData : preProcessRenderData(this.renderData)
this.doRender(this.processRenderDataWithStrictDiff(renderData))
// 重置renderData准备下次收集
@@ -476,6 +484,25 @@ export default class MpxProxy {
return result
}
+ doRenderWithVNode (vnode) {
+ if (!this._vnode) {
+ this.target.__render({ r: vnode })
+ } else {
+ let diffPath = diffAndCloneA(vnode, this._vnode).diffData
+ if (!isEmptyObject(diffPath)) {
+ // 构造 diffPath 数据
+ diffPath = Object.keys(diffPath).reduce((preVal, curVal) => {
+ const key = 'r' + curVal
+ preVal[key] = diffPath[curVal]
+ return preVal
+ }, {})
+ this.target.__render(diffPath)
+ }
+ }
+ // 缓存本地的 vnode 用以下一次 diff
+ this._vnode = diffAndCloneA(vnode).clone
+ }
+
doRender (data, cb) {
if (typeof this.target.__render !== 'function') {
error('Please specify a [__render] function to render view.', this.options.mpxFileResource)
@@ -543,15 +570,22 @@ export default class MpxProxy {
const _c = this.target._c.bind(this.target)
const _r = this.target._r.bind(this.target)
const _sc = this.target._sc.bind(this.target)
+ const _g = this.target._g.bind(this.target)
const effect = this.effect = new ReactiveEffect(() => {
// pre render for props update
if (this.propsUpdatedFlag) {
this.updatePreRender()
}
+ // const ast = this.target.dynamicRender?.() || this.getTplAst(this.moduleId)
+
+ // if (ast) {
+ // return _r(_g(ast))
+ // }
+
if (this.target.__injectedRender) {
try {
- return this.target.__injectedRender(_i, _c, _r, _sc)
+ return this.target.__injectedRender(_i, _c, _r, _sc, _g)
} catch (e) {
warn('Failed to execute render function, degrade to full-set-data mode.', this.options.mpxFileResource, e)
this.render()
diff --git a/packages/core/src/platform/builtInMixins/index.js b/packages/core/src/platform/builtInMixins/index.js
index c7c54e63f5..70523945ff 100644
--- a/packages/core/src/platform/builtInMixins/index.js
+++ b/packages/core/src/platform/builtInMixins/index.js
@@ -10,6 +10,7 @@ import pageScrollMixin from './pageScrollMixin'
import componentGenericsMixin from './componentGenericsMixin'
import getTabBarMixin from './getTabBarMixin'
import pageRouteMixin from './pageRouteMixin'
+import runtimeModulesMixin from './runtimeModulesMixin'
export default function getBuiltInMixins (options, type) {
let bulitInMixins = []
@@ -39,7 +40,8 @@ export default function getBuiltInMixins (options, type) {
bulitInMixins = bulitInMixins.concat([
renderHelperMixin(),
showMixin(type),
- i18nMixin()
+ i18nMixin(),
+ runtimeModulesMixin()
])
}
}
diff --git a/packages/core/src/platform/builtInMixins/proxyEventMixin.js b/packages/core/src/platform/builtInMixins/proxyEventMixin.js
index bf4f78bb9a..5dd9ea0412 100644
--- a/packages/core/src/platform/builtInMixins/proxyEventMixin.js
+++ b/packages/core/src/platform/builtInMixins/proxyEventMixin.js
@@ -1,5 +1,6 @@
import { setByPath, error, hasOwn, dash2hump } from '@mpxjs/utils'
import Mpx from '../../index'
+import contextMap from '../../vnode/context'
const datasetReg = /^data-(.+)$/
@@ -16,6 +17,11 @@ function collectDataset (props) {
return dataset
}
+function logCallbackNotFound (context, callbackName) {
+ const location = context.__mpxProxy && context.__mpxProxy.options.mpxFileResource
+ error(`Instance property [${callbackName}] is not function, please check.`, location)
+}
+
export default function proxyEventMixin () {
const methods = {
__invoke ($event) {
@@ -48,6 +54,9 @@ export default function proxyEventMixin () {
}
const eventConfigs = target.dataset.eventconfigs || {}
const curEventConfig = eventConfigs[type] || eventConfigs[fallbackType] || []
+ // 如果有 mpxuid 说明是运行时组件,那么需要设置对应的上下文
+ const rootRuntimeContext = contextMap.get(target.dataset.mpxuid)
+ const context = rootRuntimeContext || this
let returnedValue
curEventConfig.forEach((item) => {
const callbackName = item[0]
@@ -71,10 +80,10 @@ export default function proxyEventMixin () {
}
})
: [$event]
- if (typeof this[callbackName] === 'function') {
- returnedValue = this[callbackName].apply(this, params)
+ if (typeof context[callbackName] === 'function') {
+ returnedValue = context[callbackName].apply(context, params)
} else {
- error(`Instance property [${callbackName}] is not function, please check.`, location)
+ logCallbackNotFound(context, callbackName)
}
}
})
diff --git a/packages/core/src/platform/builtInMixins/refsMixin.js b/packages/core/src/platform/builtInMixins/refsMixin.js
index 5711a99a34..56935ef77f 100644
--- a/packages/core/src/platform/builtInMixins/refsMixin.js
+++ b/packages/core/src/platform/builtInMixins/refsMixin.js
@@ -1,5 +1,6 @@
import { CREATED, BEFORECREATE, BEFOREUPDATE, UNMOUNTED } from '../../core/innerLifecycle'
import { noop, error, getEnvObj } from '@mpxjs/utils'
+import contextMap from '../../vnode/context'
const envObj = getEnvObj()
@@ -44,6 +45,13 @@ export default function getRefsMixin () {
this.__refCacheMap.clear()
this.__asyncRefCacheMap.clear()
},
+ [CREATED] () {
+ // todo 需要确认下这部分的逻辑,如果是在 beforeCreate 钩子里面执行,部分数据取不到
+ // 如果是在运行时组件的上下文渲染
+ if (this.mpxCustomElement) {
+ this.__getRuntimeRefs()
+ }
+ },
methods: {
__getRefs () {
if (this.__getRefsData) {
@@ -72,6 +80,29 @@ export default function getRefsMixin () {
return ref.all ? this.selectAllComponents(selector) : this.selectComponent(selector)
}
}
+ },
+ __getRuntimeRefs () {
+ const vnodeContext = contextMap.get(this.uid)
+ if (vnodeContext) {
+ const refsArr = vnodeContext.__getRefsData && vnodeContext.__getRefsData()
+ if (Array.isArray(refsArr)) {
+ refsArr.forEach((ref) => {
+ const all = ref.all
+ if (!vnodeContext.$refs[ref.key] || (all && !vnodeContext.$refs[ref.key].length)) {
+ const refNode = this.__getRefNode(ref)
+ if ((all && refNode.length) || refNode) {
+ Object.defineProperty(vnodeContext.$refs, ref.key, {
+ enumerable: true,
+ configurable: true,
+ get: () => {
+ return refNode
+ }
+ })
+ }
+ }
+ })
+ }
+ }
}
}
}
diff --git a/packages/core/src/platform/builtInMixins/renderHelperMixin.js b/packages/core/src/platform/builtInMixins/renderHelperMixin.js
index c90baefa44..61b16eeed9 100644
--- a/packages/core/src/platform/builtInMixins/renderHelperMixin.js
+++ b/packages/core/src/platform/builtInMixins/renderHelperMixin.js
@@ -1,4 +1,6 @@
import { getByPath, hasOwn, isObject } from '@mpxjs/utils'
+import genVnodeTree from '../../vnode/render'
+import dynamicComponentsMap from '../../vnode/staticMap'
export default function renderHelperMixin () {
return {
@@ -36,8 +38,13 @@ export default function renderHelperMixin () {
_sc (key) {
return (this.__mpxProxy.renderData[key] = this[key])
},
- _r (skipPre) {
- this.__mpxProxy.renderWithData(skipPre)
+ _r (skipPre, vnode) {
+ this.__mpxProxy.renderWithData(skipPre, vnode)
+ },
+ _g (moduleId) {
+ const { template = {}, styles = [] } = dynamicComponentsMap[moduleId]
+ const vnodeTree = genVnodeTree(template, [this], styles, moduleId)
+ return vnodeTree
}
}
}
diff --git a/packages/core/src/platform/builtInMixins/runtimeModulesMixin.js b/packages/core/src/platform/builtInMixins/runtimeModulesMixin.js
new file mode 100644
index 0000000000..d7bc4588c9
--- /dev/null
+++ b/packages/core/src/platform/builtInMixins/runtimeModulesMixin.js
@@ -0,0 +1,33 @@
+import { CREATED } from '../../core/innerLifecycle'
+import staticMap from '../../vnode/staticMap'
+
+// todo 后续这部分的逻辑应该收敛至业务侧
+export default function getRuntimeModulesMixin () {
+ return {
+ data: {
+ __mpxDynamicLoaded: false
+ },
+ [CREATED] () {
+ const runtimeModules = this.__getRuntimeModules && this.__getRuntimeModules()
+ if (runtimeModules) {
+ // 判断是否有还未获取的组件内容
+ const moduleIds = []
+ for (const component in runtimeModules) {
+ const moduleId = runtimeModules[component]
+ if (!staticMap[moduleId]) {
+ moduleIds.push(moduleId)
+ }
+ }
+ if (typeof this.mpxLoadDynamic === 'function' && moduleIds.length) {
+ // todo: 依赖业务侧的约定,在业务侧的具体实现一个资源位对应一个 id,请求数据后在内存当中挂载
+ this.mpxLoadDynamic().then(data => {
+ this.__mpxDynamicLoaded = true
+ Object.assign(staticMap, data)
+ }).catch(e => {
+ // do something
+ })
+ }
+ }
+ }
+ }
+}
diff --git a/packages/core/src/platform/patch/ali/getDefaultOptions.js b/packages/core/src/platform/patch/ali/getDefaultOptions.js
index 70ac339880..793210057c 100644
--- a/packages/core/src/platform/patch/ali/getDefaultOptions.js
+++ b/packages/core/src/platform/patch/ali/getDefaultOptions.js
@@ -63,6 +63,16 @@ function transformApiForProxy (context, currentInject) {
}
})
}
+ if (currentInject.getRuntimeModules) {
+ Object.defineProperties(context, {
+ __getRuntimeModules: {
+ get () {
+ return currentInject.getRuntimeModules
+ },
+ configurable: false
+ }
+ })
+ }
}
}
diff --git a/packages/core/src/platform/patch/wx/getDefaultOptions.js b/packages/core/src/platform/patch/wx/getDefaultOptions.js
index b3fc09f035..c406bceda4 100644
--- a/packages/core/src/platform/patch/wx/getDefaultOptions.js
+++ b/packages/core/src/platform/patch/wx/getDefaultOptions.js
@@ -101,6 +101,16 @@ function transformApiForProxy (context, currentInject) {
}
})
}
+ if (currentInject.getRuntimeModules) {
+ Object.defineProperties(context, {
+ __getRuntimeModules: {
+ get () {
+ return currentInject.getRuntimeModules
+ },
+ configurable: false
+ }
+ })
+ }
}
}
diff --git a/packages/core/src/vnode/context.js b/packages/core/src/vnode/context.js
new file mode 100644
index 0000000000..0600c3e41e
--- /dev/null
+++ b/packages/core/src/vnode/context.js
@@ -0,0 +1,17 @@
+const cache = {}
+
+const contextMap = {
+ set (id, context) {
+ cache[id] = context
+ },
+ get (id) {
+ return cache[id]
+ },
+ remove (id) {
+ if (cache[id]) {
+ delete cache[id]
+ }
+ }
+}
+
+export default contextMap
diff --git a/packages/core/src/vnode/css-select/cssauron.js b/packages/core/src/vnode/css-select/cssauron.js
new file mode 100644
index 0000000000..65a3aaa12f
--- /dev/null
+++ b/packages/core/src/vnode/css-select/cssauron.js
@@ -0,0 +1,445 @@
+import tokenizer from './tokenizer'
+
+export default function language (lookups, matchComparison) {
+ return function (selector, moduleId) {
+ return parse(
+ selector,
+ remap(lookups),
+ moduleId,
+ matchComparison || caseSensitiveComparison
+ )
+ }
+}
+
+function remap (opts) {
+ // 对于字符串类型的 value 转化为函数
+ for (const key in opts) {
+ if (opt_okay(opts, key)) {
+ /* eslint-disable-next-line */
+ opts[key] = Function(
+ 'return function(node, attr) { return node.' + opts[key] + ' }'
+ )
+ opts[key] = opts[key]()
+ }
+ }
+
+ return opts
+}
+
+function opt_okay (opts, key) {
+ return Object.prototype.hasOwnProperty.call(opts, key) && typeof opts[key] === 'string'
+}
+
+function parse (selector, options, moduleId, matchComparison) {
+ const stream = tokenizer()
+ // const default_subj = true
+ const selectors = [[]]
+ let bits = selectors[0]
+
+ // 逆向关系
+ const traversal = {
+ '': any_parents,
+ '>': direct_parent,
+ '+': direct_sibling,
+ '~': any_sibling
+ }
+
+ stream.on('data', group)
+ stream.end(selector)
+
+ function group (token) {
+ if (token.type === 'comma') {
+ selectors.unshift((bits = []))
+
+ return
+ }
+
+ // 获取节点之间的关系路径,匹配的规则从右往左依次进行,因此在后面的匹配规则需要存储在栈结构的前面
+ if (token.type === 'op' || token.type === 'any-child') {
+ bits.unshift(traversal[token.data]) // 获取节点之间关系的操作数
+ bits.unshift(check()) // 添加空的匹配操作数
+
+ return
+ }
+
+ bits[0] = bits[0] || check()
+ const crnt = bits[0]
+
+ if (token.type === '!') {
+ crnt.subject = selectors[0].subject = true
+
+ return
+ }
+
+ crnt.push(
+ token.type === 'class'
+ ? listContains(token.type, token.data)
+ : token.type === 'attr'
+ ? attr(token)
+ : token.type === ':' || token.type === '::'
+ ? pseudo(token)
+ : token.type === '*'
+ ? Boolean
+ : matches(token.type, token.data, matchComparison)
+ )
+ }
+
+ return selector_fn
+
+ // 单节点对比
+ function selector_fn (node, as_boolean) {
+ if (node.data?.moduleId !== moduleId) {
+ return
+ }
+ let current, length, subj
+
+ const orig = node
+ const set = []
+
+ for (let i = 0, len = selectors.length; i < len; ++i) {
+ bits = selectors[i]
+ current = entry // 当前 bits 检测规则
+ length = bits.length
+ node = orig // 引用赋值
+ subj = []
+
+ let j = 0
+ // 步长为2,因为2个节点之间的关系中间会有一个 OP 操作符
+ for (j = 0; j < length; j += 2) {
+ node = current(node, bits[j], subj)
+
+ if (!node) {
+ break
+ }
+
+ // todo 这里的规则和步长设计的很巧妙
+ current = bits[j + 1] // 改变当前的 bits 检测规则
+ }
+
+ if (j >= length) {
+ if (as_boolean) {
+ return true
+ }
+
+ add(!bits.subject ? [orig] : subj)
+ }
+ }
+
+ if (as_boolean) {
+ return false
+ }
+
+ return !set.length ? false : set.length === 1 ? set[0] : set
+
+ function add (items) {
+ let next
+
+ while (items.length) {
+ next = items.shift()
+
+ if (set.indexOf(next) === -1) {
+ set.push(next)
+ }
+ }
+ }
+ }
+
+ // 匹配操作数
+ function check () {
+ _check.bits = []
+ _check.subject = false
+ _check.push = function (token) {
+ _check.bits.push(token)
+ }
+
+ return _check
+
+ function _check (node, subj) {
+ for (let i = 0, len = _check.bits.length; i < len; ++i) {
+ if (!_check.bits[i](node)) {
+ return false
+ }
+ }
+
+ if (_check.subject) {
+ subj.push(node)
+ }
+
+ return true
+ }
+ }
+
+ function listContains (type, data) {
+ return function (node) {
+ let val = options[type](node)
+ val = Array.isArray(val) ? val : val ? val.toString().split(/\s+/) : []
+ return val.indexOf(data) >= 0
+ }
+ }
+
+ function attr (token) {
+ return token.data.lhs
+ ? valid_attr(options.attr, token.data.lhs, token.data.cmp, token.data.rhs)
+ : valid_attr(options.attr, token.data)
+ }
+
+ function matches (type, data, matchComparison) {
+ return function (node) {
+ return matchComparison(type, options[type](node), data)
+ }
+ }
+
+ function any_parents (node, next, subj) {
+ do {
+ node = options.parent(node)
+ } while (node && !next(node, subj))
+
+ return node
+ }
+
+ function direct_parent (node, next, subj) {
+ node = options.parent(node)
+
+ return node && next(node, subj) ? node : null
+ }
+
+ function direct_sibling (node, next, subj) {
+ const parent = options.parent(node)
+ let idx = 0
+
+ const children = options.children(parent)
+
+ for (let i = 0, len = children.length; i < len; ++i) {
+ if (children[i] === node) {
+ idx = i
+
+ break
+ }
+ }
+
+ return children[idx - 1] && next(children[idx - 1], subj)
+ ? children[idx - 1]
+ : null
+ }
+
+ function any_sibling (node, next, subj) {
+ const parent = options.parent(node)
+
+ const children = options.children(parent)
+
+ for (let i = 0, len = children.length; i < len; ++i) {
+ if (children[i] === node) {
+ return null
+ }
+
+ if (next(children[i], subj)) {
+ return children[i]
+ }
+ }
+
+ return null
+ }
+
+ function pseudo (token) {
+ return valid_pseudo(options, token.data, matchComparison)
+ }
+}
+
+function entry (node, next, subj) {
+ return next(node, subj) ? node : null
+}
+
+function valid_pseudo (options, match, matchComparison) {
+ switch (match) {
+ case 'empty':
+ return valid_empty(options)
+ case 'first-child':
+ return valid_first_child(options)
+ case 'last-child':
+ return valid_last_child(options)
+ case 'root':
+ return valid_root(options)
+ }
+
+ if (match.indexOf('contains') === 0) {
+ return valid_contains(options, match.slice(9, -1))
+ }
+
+ if (match.indexOf('any') === 0) {
+ return valid_any_match(options, match.slice(4, -1), matchComparison)
+ }
+
+ if (match.indexOf('not') === 0) {
+ return valid_not_match(options, match.slice(4, -1), matchComparison)
+ }
+
+ if (match.indexOf('nth-child') === 0) {
+ return valid_nth_child(options, match.slice(10, -1))
+ }
+
+ return function () {
+ return false
+ }
+}
+
+function valid_not_match (options, selector, matchComparison) {
+ const fn = parse(selector, options, matchComparison)
+
+ return not_function
+
+ function not_function (node) {
+ return !fn(node, true)
+ }
+}
+
+function valid_any_match (options, selector, matchComparison) {
+ const fn = parse(selector, options, matchComparison)
+
+ return fn
+}
+
+function valid_attr (fn, lhs, cmp, rhs) {
+ return function (node) {
+ const attr = fn(node, lhs)
+
+ if (!cmp) {
+ return !!attr
+ }
+
+ if (cmp.length === 1) {
+ return attr === rhs
+ }
+
+ if (attr === undefined || attr === null) {
+ return false
+ }
+
+ return checkattr[cmp.charAt(0)](attr, rhs)
+ }
+}
+
+function valid_first_child (options) {
+ return function (node) {
+ return options.children(options.parent(node))[0] === node
+ }
+}
+
+function valid_last_child (options) {
+ return function (node) {
+ const children = options.children(options.parent(node))
+
+ return children[children.length - 1] === node
+ }
+}
+
+function valid_empty (options) {
+ return function (node) {
+ return options.children(node).length === 0
+ }
+}
+
+function valid_root (options) {
+ return function (node) {
+ return !options.parent(node)
+ }
+}
+
+function valid_contains (options, contents) {
+ return function (node) {
+ return options.contents(node).indexOf(contents) !== -1
+ }
+}
+
+function valid_nth_child (options, nth) {
+ let test = function () {
+ return false
+ }
+ if (nth === 'odd') {
+ nth = '2n+1'
+ } else if (nth === 'even') {
+ nth = '2n'
+ }
+ const regexp = /( ?([-|+])?(\d*)n)? ?((\+|-)? ?(\d+))? ?/
+ const matches = nth.match(regexp)
+ if (matches) {
+ let growth = 0
+ if (matches[1]) {
+ const positiveGrowth = matches[2] !== '-'
+ growth = parseInt(matches[3] === '' ? 1 : matches[3])
+ growth = growth * (positiveGrowth ? 1 : -1)
+ }
+ let offset = 0
+ if (matches[4]) {
+ offset = parseInt(matches[6])
+ const positiveOffset = matches[5] !== '-'
+ offset = offset * (positiveOffset ? 1 : -1)
+ }
+ if (growth === 0) {
+ if (offset !== 0) {
+ test = function (children, node) {
+ return children[offset - 1] === node
+ }
+ }
+ } else {
+ test = function (children, node) {
+ const validPositions = []
+ const len = children.length
+ for (let position = 1; position <= len; position++) {
+ const divisible = (position - offset) % growth === 0
+ if (divisible) {
+ if (growth > 0) {
+ validPositions.push(position)
+ } else {
+ if ((position - offset) / growth >= 0) {
+ validPositions.push(position)
+ }
+ }
+ }
+ }
+ for (let i = 0; i < validPositions.length; i++) {
+ if (children[validPositions[i] - 1] === node) {
+ return true
+ }
+ }
+ return false
+ }
+ }
+ }
+ return function (node) {
+ const children = options.children(options.parent(node))
+
+ return test(children, node)
+ }
+}
+
+const checkattr = {
+ $: check_end,
+ '^': check_beg,
+ '*': check_any,
+ '~': check_spc,
+ '|': check_dsh
+}
+
+function check_end (l, r) {
+ return l.slice(l.length - r.length) === r
+}
+
+function check_beg (l, r) {
+ return l.slice(0, r.length) === r
+}
+
+function check_any (l, r) {
+ return l.indexOf(r) > -1
+}
+
+function check_spc (l, r) {
+ return l.split(/\s+/).indexOf(r) > -1
+}
+
+function check_dsh (l, r) {
+ return l.split('-').indexOf(r) > -1
+}
+
+function caseSensitiveComparison (type, pattern, data) {
+ return pattern === data
+}
diff --git a/packages/core/src/vnode/css-select/index.js b/packages/core/src/vnode/css-select/index.js
new file mode 100644
index 0000000000..a1a8e2320c
--- /dev/null
+++ b/packages/core/src/vnode/css-select/index.js
@@ -0,0 +1,118 @@
+import cssauron from './cssauron'
+
+const language = cssauron({
+ tag: function (node) {
+ return node.nodeType
+ },
+ class: function (node) {
+ return node.data?.class
+ },
+ id: function (node) {
+ return node.data?.id
+ },
+ children: function (node) {
+ return node.children
+ },
+ parent: function (node) {
+ // todo 不能返回默认值,会导致 cssauron 匹配过程找父节点出现死循环
+ // return node.parent || {}
+ return node.parent
+ },
+ contents: function (node) {
+ return node.contents || ''
+ },
+ attr: function (node, attr) {
+ if (node.properties) {
+ const attrs = node.properties.attributes
+ if (attrs && attrs[attr]) {
+ return attrs[attr]
+ }
+ return node.properties[attr]
+ }
+ }
+})
+
+export default function cssSelect (sel, options) {
+ options = options || {}
+ const selector = language(sel, options.moduleId)
+ function match (vtree) {
+ const node = mapTree(vtree, null, options) || {}
+ const matched = []
+
+ // Traverse each node in the tree and see if it matches our selector
+ traverse(node, function (node) {
+ let result = selector(node)
+ if (result) {
+ if (!Array.isArray(result)) {
+ result = [result]
+ }
+ matched.push.apply(matched, result)
+ }
+ })
+
+ const results = mapResult(matched)
+ if (results.length === 0) {
+ return null
+ }
+ return results
+ }
+ match.matches = function (vtree) {
+ const node = mapTree(vtree, null, options)
+ return !!selector(node)
+ }
+ return match
+}
+
+function traverse (vtree, fn) {
+ fn(vtree)
+ if (vtree.children) {
+ vtree.children.forEach(function (vtree) {
+ traverse(vtree, fn)
+ })
+ }
+}
+
+function mapResult (result) {
+ return result
+ .filter(function (node) {
+ return !!node.vtree
+ })
+ .map(function (node) {
+ return node.vtree
+ })
+}
+
+function getNormalizeCaseFn (caseSensitive) {
+ return caseSensitive
+ ? function noop (str) {
+ return str
+ }
+ : function toLowerCase (str) {
+ return str.toLowerCase()
+ }
+}
+
+// Map a virtual-dom node tree into a data structure that cssauron can use to
+// traverse.
+function mapTree (vtree, parent, options) {
+ const normalizeTagCase = getNormalizeCaseFn(options.caseSensitiveTag)
+
+ if (vtree.nt != null) {
+ const node = {}
+ node.parent = parent
+ node.vtree = vtree
+ node.nodeType = normalizeTagCase(vtree.nt)
+ if (vtree.d) {
+ node.data = vtree.d
+ }
+
+ if (vtree.c) {
+ node.children = vtree.c
+ .map(function (child) {
+ return mapTree(child, node, options)
+ })
+ .filter(Boolean)
+ }
+ return node
+ }
+}
diff --git a/packages/core/src/vnode/css-select/through.js b/packages/core/src/vnode/css-select/through.js
new file mode 100644
index 0000000000..654a349c06
--- /dev/null
+++ b/packages/core/src/vnode/css-select/through.js
@@ -0,0 +1,19 @@
+export default function through (onData) {
+ let dataCb = null
+
+ return {
+ on: function (name, callback) {
+ if (name === 'data') {
+ dataCb = callback
+ }
+ },
+ end: function (data) {
+ onData(data)
+ },
+ queue: function (data) {
+ if (dataCb) {
+ dataCb(data)
+ }
+ }
+ }
+}
diff --git a/packages/core/src/vnode/css-select/tokenizer.js b/packages/core/src/vnode/css-select/tokenizer.js
new file mode 100644
index 0000000000..ba0094c53b
--- /dev/null
+++ b/packages/core/src/vnode/css-select/tokenizer.js
@@ -0,0 +1,371 @@
+import through from './through'
+
+const PSEUDOSTART = 'pseudo-start'
+const ATTR_START = 'attr-start'
+const ANY_CHILD = 'any-child'
+const ATTR_COMP = 'attr-comp'
+const ATTR_END = 'attr-end'
+const PSEUDOPSEUDO = '::'
+const PSEUDOCLASS = ':'
+const READY = '(ready)' // 重置标志位
+const OPERATION = 'op'
+const CLASS = 'class'
+const COMMA = 'comma'
+const ATTR = 'attr'
+const SUBJECT = '!'
+const TAG = 'tag'
+const STAR = '*'
+const ID = 'id'
+
+export default function tokenize () {
+ let escaped = false
+ let gathered = []
+ let state = READY
+ let data = []
+ let idx = 0
+ let stream
+ let length
+ let quote
+ let depth
+ let lhs
+ let rhs
+ let cmp
+ let c
+
+ return (stream = through(ondata, onend))
+
+ function ondata (chunk) {
+ data = data.concat(chunk.split(''))
+ length = data.length
+
+ while (idx < length && (c = data[idx++])) {
+ switch (state) {
+ case READY:
+ state_ready()
+ break
+ case ANY_CHILD:
+ state_any_child()
+ break
+ case OPERATION:
+ state_op()
+ break
+ case ATTR_START:
+ state_attr_start()
+ break
+ case ATTR_COMP:
+ state_attr_compare()
+ break
+ case ATTR_END:
+ state_attr_end()
+ break
+ case PSEUDOCLASS:
+ case PSEUDOPSEUDO:
+ state_pseudo()
+ break
+ case PSEUDOSTART:
+ state_pseudostart()
+ break
+ case ID:
+ case TAG:
+ case CLASS:
+ state_gather()
+ break
+ }
+ }
+
+ data = data.slice(idx)
+
+ if (gathered.length) {
+ stream.queue(token())
+ }
+ }
+
+ function onend (chunk) {
+ // if (arguments.length) {
+ // ondata(chunk)
+ // }
+
+ // if (gathered.length) {
+ // stream.queue(token())
+ // }
+ }
+
+ function state_ready () {
+ switch (true) {
+ case c === '#':
+ state = ID
+ break
+ case c === '.':
+ state = CLASS
+ break
+ case c === ':':
+ state = PSEUDOCLASS
+ break
+ case c === '[':
+ state = ATTR_START
+ break
+ case c === '!':
+ subject()
+ break
+ case c === '*':
+ star()
+ break
+ case c === ',':
+ comma()
+ break
+ case /[>+~]/.test(c):
+ state = OPERATION
+ break
+ case /\s/.test(c):
+ state = ANY_CHILD
+ break
+ case /[\w\d\-_]/.test(c):
+ state = TAG
+ --idx
+ break
+ }
+ }
+
+ function subject () {
+ state = SUBJECT
+ gathered = ['!']
+ stream.queue(token())
+ state = READY
+ }
+
+ function star () {
+ state = STAR
+ gathered = ['*']
+ stream.queue(token())
+ state = READY
+ }
+
+ function comma () {
+ state = COMMA
+ gathered = [',']
+ stream.queue(token())
+ state = READY
+ }
+
+ function state_op () {
+ if (/[>+~]/.test(c)) {
+ return gathered.push(c)
+ }
+
+ // chomp down the following whitespace.
+ if (/\s/.test(c)) {
+ return
+ }
+
+ stream.queue(token())
+ state = READY
+ --idx // 指针左移,归档,开始匹配下一个 token
+ }
+
+ function state_any_child () {
+ if (/\s/.test(c)) {
+ return
+ }
+
+ if (/[>+~]/.test(c)) {
+ --idx
+ state = OPERATION
+ return state
+ // return --idx, (state = OPERATION)
+ }
+
+ // 生成 any_child 节点,并重置状态
+ stream.queue(token())
+ state = READY
+ --idx
+ }
+
+ function state_pseudo () {
+ rhs = state
+ state_gather(true)
+
+ if (state !== READY) {
+ return
+ }
+
+ if (c === '(') {
+ lhs = gathered.join('')
+ state = PSEUDOSTART
+ gathered.length = 0
+ depth = 1
+ ++idx
+
+ return
+ }
+
+ state = PSEUDOCLASS
+ stream.queue(token())
+ state = READY
+ }
+
+ function state_pseudostart () {
+ if (gathered.length === 0 && !quote) {
+ quote = /['"]/.test(c) ? c : null
+
+ if (quote) {
+ return
+ }
+ }
+
+ if (quote) {
+ if (!escaped && c === quote) {
+ quote = null
+
+ return
+ }
+
+ if (c === '\\') {
+ escaped ? gathered.push(c) : (escaped = true)
+
+ return
+ }
+
+ escaped = false
+ gathered.push(c)
+
+ return
+ }
+
+ gathered.push(c)
+
+ if (c === '(') {
+ ++depth
+ } else if (c === ')') {
+ --depth
+ }
+
+ if (!depth) {
+ gathered.pop()
+ stream.queue({
+ type: rhs,
+ data: lhs + '(' + gathered.join('') + ')'
+ })
+
+ state = READY
+ lhs = rhs = cmp = null
+ gathered.length = 0
+ }
+ }
+
+ function state_attr_start () {
+ // 在收集字符的阶段,还会有 state 标志位的判断,因此会影响到下面的逻辑执行
+ state_gather(true)
+
+ if (state !== READY) {
+ return
+ }
+
+ if (c === ']') {
+ state = ATTR
+ stream.queue(token())
+ state = READY
+
+ return
+ }
+
+ lhs = gathered.join('')
+ gathered.length = 0
+ state = ATTR_COMP
+ }
+
+ // 属性选择器:https://www.w3school.com.cn/css/css_attribute_selectors.asp
+ function state_attr_compare () {
+ if (/[=~|$^*]/.test(c)) {
+ gathered.push(c)
+ }
+
+ // 操作符&=
+ if (gathered.length === 2 || c === '=') {
+ cmp = gathered.join('')
+ gathered.length = 0
+ state = ATTR_END
+ quote = null
+ }
+ }
+
+ function state_attr_end () {
+ if (!gathered.length && !quote) {
+ quote = /['"]/.test(c) ? c : null
+
+ if (quote) {
+ return
+ }
+ }
+
+ if (quote) {
+ if (!escaped && c === quote) {
+ quote = null
+
+ return
+ }
+
+ if (c === '\\') {
+ if (escaped) {
+ gathered.push(c)
+ }
+
+ escaped = !escaped
+
+ return
+ }
+
+ escaped = false
+ gathered.push(c)
+
+ return
+ }
+
+ state_gather(true)
+
+ if (state !== READY) {
+ return
+ }
+
+ stream.queue({
+ type: ATTR,
+ data: {
+ lhs: lhs,
+ rhs: gathered.join(''),
+ cmp: cmp
+ }
+ })
+
+ state = READY
+ lhs = rhs = cmp = null
+ gathered.length = 0
+ }
+
+ function state_gather (quietly) {
+ // 如果是非单词字符,例如 空格。会更新 state 的状态
+ if (/[^\d\w\-_]/.test(c) && !escaped) {
+ if (c === '\\') {
+ escaped = true
+ } else {
+ !quietly && stream.queue(token())
+ state = READY
+ --idx
+ }
+
+ return
+ }
+
+ escaped = false
+ gathered.push(c)
+ }
+
+ function token () {
+ const data = gathered.join('')
+
+ gathered.length = 0
+
+ return {
+ type: state,
+ data: data
+ }
+ }
+}
diff --git a/packages/core/src/vnode/interpreter.js b/packages/core/src/vnode/interpreter.js
new file mode 100644
index 0000000000..9775d000ad
--- /dev/null
+++ b/packages/core/src/vnode/interpreter.js
@@ -0,0 +1,449 @@
+class Interpreter {
+ constructor (contextScope = []) {
+ this.stateStack = []
+ this.value = undefined
+ contextScope.unshift(this.initGlobalContext())
+ this.contextScope = contextScope
+
+ this.TYPE_ERROR = 'TypeError'
+ this.REFERENCE_ERROR = 'ReferenceError'
+ }
+
+ eval (ast) {
+ const state = new State(ast, {})
+ this.stateStack = [state]
+ // eslint-disable-next-line
+ while (this.step()) { }
+ return this.value
+ }
+
+ step () {
+ const state = this.stateStack[this.stateStack.length - 1]
+ if (!state) {
+ return false
+ }
+ const node = state.node
+ const type = node[0]
+ // Program
+ if (type === 1 && state.done) {
+ return false
+ }
+
+ let nextState
+ try {
+ nextState = this[type](this.stateStack, state, node)
+ } catch (e) {
+ throw Error(e)
+ }
+
+ if (nextState) {
+ this.stateStack.push(nextState)
+ }
+ return true
+ }
+
+ getValue (ref) {
+ if (ref[0] === Interpreter.SCOPE_REFERENCE) {
+ // A null/varname variable lookup
+ return this.getValueFromScope(ref[1])
+ } else {
+ // An obj/prop components tuple(foo.bar)
+ return ref[0][ref[1]]
+ }
+ }
+
+ setValue (ref, value) {
+ if (ref[0] === Interpreter.SCOPE_REFERENCE) {
+ return this.setValueToScope(ref[1], value)
+ } else {
+ ref[0][ref[1]] = value
+ return value
+ }
+ }
+
+ setValueToScope (name, value) {
+ let index = this.contextScope.length
+ while (index--) {
+ const scope = this.contextScope[index]
+ if (name in scope) {
+ scope[name] = value
+ return undefined
+ }
+ }
+ this.throwException(this.REFERENCE_ERROR, name + ' is not defined')
+ }
+
+ getValueFromScope (name) {
+ let index = this.contextScope.length
+ while (index--) {
+ const scope = this.contextScope[index]
+ if (name in scope) {
+ return scope[name]
+ }
+ }
+ this.throwException(this.REFERENCE_ERROR, name + ' is not defined')
+ }
+
+ throwException (errorType, message) {
+ throw new Error('[JsInterpreter]: ' + errorType + ` ${message}.`)
+ }
+
+ initGlobalContext () {
+ const context = {
+ // eslint-disable-next-line
+ 'undefined': undefined
+ }
+ return context
+ }
+}
+
+Interpreter.SCOPE_REFERENCE = { SCOPE_REFERENCE: true }
+Interpreter.STEP_ERROR = { STEP_ERROR: true }
+
+class State {
+ constructor (node, scope) {
+ this.node = node
+ this.scope = scope
+ }
+}
+
+// Program
+Interpreter.prototype[1] = function stepProgram (stack, state, node) {
+ const bodyIndex = 1
+ const expression = node[bodyIndex].shift()
+ if (expression) {
+ state.done = false
+ return new State(expression)
+ }
+ state.done = true
+}
+
+// Identifier
+Interpreter.prototype[2] = function stepIdentifier (stack, state, node) {
+ const identifierIndex = 1
+ stack.pop()
+ // 引用场景: ++a
+ if (state.components) {
+ stack[stack.length - 1].value = [Interpreter.SCOPE_REFERENCE, node[identifierIndex]]
+ return
+ }
+ const value = this.getValueFromScope(node[identifierIndex])
+ stack[stack.length - 1].value = value
+}
+
+// Literal 暂未支持正则字面量,也不需要支持
+Interpreter.prototype[3] = function stepLiteral (stack, state, node) {
+ stack.pop()
+ stack[stack.length - 1].value = node[1]
+}
+
+// ArrayExpression
+Interpreter.prototype[28] = function stepArrayExpression (stack, state, node) {
+ const elementsIndex = 1
+ const elements = node[elementsIndex]
+ let n = state.n_ || 0
+ if (!state.array_) {
+ state.array_ = []
+ } else {
+ state.array_[n] = state.value
+ n++
+ }
+ while (n < elements.length) {
+ if (elements[n]) {
+ state.n_ = n
+ return new State(elements[n])
+ }
+ n++
+ }
+ stack.pop()
+ stack[stack.length - 1].value = state.array_
+}
+
+// ObjectExpression
+Interpreter.prototype[29] = function stepObjectExpression (stack, state, node) {
+ const propertyIndex = 1
+ const kindIndex = 3
+ let n = state.n_ || 0
+ let property = node[propertyIndex][n]
+ if (!state.object_) {
+ // first execution
+ state.object_ = {}
+ state.properties_ = {}
+ } else {
+ const propName = state.destinationName
+ if (!state.properties_[propName]) {
+ state.properties_[propName] = {}
+ }
+ state.properties_[propName][property[kindIndex]] = state.value
+ state.n_ = ++n
+ property = node[propertyIndex][n]
+ }
+
+ if (property) {
+ const keyIndex = 1
+ const valueIndex = 2
+ const key = property[keyIndex]
+ const identifierOrLiteralValueIndex = 1
+ let propName
+ if (key[0] === 2 || key[0] === 3) {
+ propName = key[identifierOrLiteralValueIndex]
+ } else {
+ throw SyntaxError('Unknown object structure: ' + key[0])
+ }
+ state.destinationName = propName
+ return new State(property[valueIndex])
+ }
+
+ for (const key in state.properties_) {
+ state.object_[key] = state.properties_[key].init
+ }
+ stack.pop()
+ stack[stack.length - 1].value = state.object_
+}
+
+// UnaryExpression
+Interpreter.prototype[33] = function stepUnaryExpression (stack, state, node) {
+ const operatorIndex = 1
+ const argumentIndex = 2
+ if (!state.done_) {
+ state.done_ = true
+ const nextState = new State(node[argumentIndex])
+ nextState.components = node[operatorIndex] === 'delete'
+ return nextState
+ }
+ stack.pop()
+ let value = state.value
+ switch (node[operatorIndex]) {
+ case '-':
+ value = -value
+ break
+ case '+':
+ value = +value
+ break
+ case '!':
+ value = !value
+ break
+ case '~':
+ value = ~value
+ break
+ case 'delete': {
+ let result = true
+ if (Array.isArray(value)) {
+ let context = value[0]
+ if (context === Interpreter.SCOPE_REFERENCE) {
+ context = this.contextScope
+ }
+ const name = String(value[1])
+ try {
+ delete context[0][name]
+ } catch (e) {
+ this.throwException(this.TYPE_ERROR, "Cannot delete property '" + name + "' of '" + context[0] + "'")
+ result = false
+ }
+ }
+ value = result
+ break
+ }
+ case 'typeof':
+ value = typeof value
+ break
+ case 'void':
+ value = undefined
+ break
+ default:
+ throw SyntaxError('Unknow unary operator:' + node[operatorIndex])
+ }
+ stack[stack.length - 1].value = value
+}
+
+// UpdateExpression
+Interpreter.prototype[34] = function stepUpdateExpression (stack, state, node) {
+ const argumentIndex = 2
+ if (!state.doneLeft_) {
+ state.doneLeft_ = true
+ const nextState = new State(node[argumentIndex])
+ nextState.components = true
+ return nextState
+ }
+
+ if (!state.leftSide_) {
+ state.leftSide_ = state.value
+ }
+ if (!state.doneGetter_) {
+ const leftValue = this.getValue(state.leftSide_)
+ state.leftValue_ = leftValue
+ }
+
+ const operatorIndex = 1
+ const leftValue = Number(state.leftValue_)
+ let changeValue
+ if (node[operatorIndex] === '++') {
+ changeValue = leftValue + 1
+ } else if (node[operatorIndex] === '--') {
+ changeValue = leftValue - 1
+ } else {
+ throw SyntaxError('Unknown update expression: ' + node[operatorIndex])
+ }
+ const prefixIndex = 3
+ const returnValue = node[prefixIndex] ? changeValue : leftValue
+ this.setValue(state.leftSide_, changeValue)
+
+ stack.pop()
+ stack[stack.length - 1].value = returnValue
+}
+
+// BinaryExpression
+Interpreter.prototype[35] = function stepBinaryExpression (stack, state, node) {
+ if (!state.doneLeft_) {
+ state.doneLeft_ = true
+ const leftNodeIndex = 2
+ return new State(node[leftNodeIndex])
+ }
+ if (!state.doneRight_) {
+ state.doneRight_ = true
+ state.leftValue_ = state.value
+ const rightNodeIndex = 3
+ return new State(node[rightNodeIndex])
+ }
+ stack.pop()
+ const leftValue = state.leftValue_
+ const rightValue = state.value
+ const operatorIndex = 1
+ let value
+ switch (node[operatorIndex]) {
+ // eslint-disable-next-line
+ case '==': value = leftValue == rightValue; break
+ // eslint-disable-next-line
+ case '!=': value = leftValue != rightValue; break
+ case '===': value = leftValue === rightValue; break
+ case '!==': value = leftValue !== rightValue; break
+ case '>': value = leftValue > rightValue; break
+ case '>=': value = leftValue >= rightValue; break
+ case '<': value = leftValue < rightValue; break
+ case '<=': value = leftValue <= rightValue; break
+ case '+': value = leftValue + rightValue; break
+ case '-': value = leftValue - rightValue; break
+ case '*': value = leftValue * rightValue; break
+ case '/': value = leftValue / rightValue; break
+ case '%': value = leftValue % rightValue; break
+ case '&': value = leftValue & rightValue; break
+ case '|': value = leftValue | rightValue; break
+ case '^': value = leftValue ^ rightValue; break
+ case '<<': value = leftValue << rightValue; break
+ case '>>': value = leftValue >> rightValue; break
+ case '>>>': value = leftValue >>> rightValue; break
+ case 'in':
+ if (!(rightValue instanceof Object)) {
+ this.throwException(this.TYPE_ERROR, "'in' expects an object, not '" + rightValue + "'")
+ }
+ value = leftValue in rightValue
+ break
+ case 'instanceof':
+ if (!(rightValue instanceof Object)) {
+ this.throwException(this.TYPE_ERROR, 'Right-hand side of instanceof is not an object')
+ }
+ value = leftValue instanceof rightValue
+ break
+ default:
+ throw SyntaxError('Unknown binary operator: ' + node[operatorIndex])
+ }
+ stack[stack.length - 1].value = value
+}
+
+// LogicalExpression
+Interpreter.prototype[37] = function stepLogicalExpression (stack, state, node) {
+ const operatorIndex = 1
+ const leftIndex = 2
+ const rightIndex = 3
+ if (node[operatorIndex] !== '&&' && node[operatorIndex] !== '||') {
+ throw SyntaxError('Unknown logical operator: ' + node[operatorIndex])
+ }
+ if (!state.doneLeft_) {
+ state.doneLeft_ = true
+ return new State(node[leftIndex])
+ }
+
+ if (!state.doneRight_) {
+ state.doneRight_ = true
+ // Shortcut evaluation
+ if ((node[operatorIndex] === '&&' && !state.value) || (node[operatorIndex] === '||' && state.value)) {
+ stack.pop()
+ stack[stack.length - 1].value = state.value
+ } else {
+ state.doneRight_ = true
+ return new State(node[rightIndex])
+ }
+ } else {
+ stack.pop()
+ stack[stack.length - 1].value = state.value
+ }
+}
+
+// MemberExpression
+Interpreter.prototype[38] = function stepMemberExperssion (stack, state, node) {
+ const objectIndex = 1
+ if (!state.doneObject_) {
+ state.doneObject_ = true
+ return new State(node[objectIndex])
+ }
+
+ const computedIndex = 3
+ const propertyIndex = 2
+ const propertyKeyIndex = 1
+ let propName
+ if (!node[computedIndex]) {
+ state.object_ = state.value
+ // obj.foo -- Just access `foo` directly.
+ propName = node[propertyIndex][propertyKeyIndex]
+ } else if (!state.doneProperty_) {
+ state.object_ = state.value
+ // obj[foo] -- Compute value of `foo`.
+ state.doneProperty_ = true
+ return new State(node[propertyIndex])
+ } else {
+ propName = state.value
+ }
+ // todo 取值的优化,错误提示等
+ const value = state.object_[propName]
+ stack.pop()
+ stack[stack.length - 1].value = value
+}
+
+// ConditionalExpression
+Interpreter.prototype[39] = function stepConditionalExpression (stack, state, node) {
+ const testIndex = 1
+ const mode = state.mode_ || 0
+ if (mode === 0) {
+ state.mode_ = 1
+ return new State(node[testIndex])
+ }
+ if (mode === 1) {
+ state.mode_ = 2
+ const value = Boolean(state.value)
+ const consequentIndex = 2
+ const alternateIndex = 3
+ if (value && node[consequentIndex]) {
+ return new State(node[consequentIndex])
+ } else if (!value && node[alternateIndex]) {
+ return new State(node[alternateIndex])
+ }
+ }
+ stack.pop()
+ if (node[0] === 39) {
+ stack[stack.length - 1].value = state.value
+ }
+}
+
+// ExpressionStatement
+Interpreter.prototype[40] = function stepExpressionStatement (stack, state, node) {
+ const expressionIndex = 1
+ if (!state.done_) {
+ state.done_ = true
+ return new State(node[expressionIndex])
+ }
+ stack.pop()
+ // Save this value to interpreter.value for use as a return value
+ this.value = state.value
+}
+
+export default Interpreter
diff --git a/packages/core/src/vnode/render.js b/packages/core/src/vnode/render.js
new file mode 100644
index 0000000000..362b315405
--- /dev/null
+++ b/packages/core/src/vnode/render.js
@@ -0,0 +1,314 @@
+import cssSelect from './css-select'
+// todo: stringify wxs 模块只能放到逻辑层执行,主要还是因为生成 vdom tree 需要根据 class 去做匹配,需要看下这个代码从哪引入
+import stringify from '../../../webpack-plugin/lib/runtime/stringify.wxs'
+import Interpreter from './interpreter'
+import { dash2hump, isString } from '@mpxjs/utils'
+
+const deepCloneNode = function (val) {
+ return JSON.parse(JSON.stringify(val))
+}
+
+function simpleNormalizeChildren (children) {
+ for (let i = 0; i < children.length; i++) {
+ if (Array.isArray(children[i])) {
+ return Array.prototype.concat.apply([], children)
+ }
+ }
+ return children
+}
+
+function cloneNode (el) {
+ const clone = Object.assign({}, el)
+ if (el.parent) clone.parent = null
+ if (el.children) {
+ clone.children = []
+ el.children.forEach((child) => {
+ addChild(clone, cloneNode(child))
+ })
+ }
+ return clone
+}
+
+function addChild (parent, newChild, before) {
+ parent.children = parent.children || []
+ if (before) {
+ parent.children.unshift(newChild)
+ } else {
+ parent.children.push(newChild)
+ }
+}
+
+export default function _genVnodeTree (vnodeAst, contextScope, cssList, moduleId) {
+ // 引用的 vnodeAst 浅复制,解除引用
+ vnodeAst = cloneNode(vnodeAst)
+ // 获取实例 uid
+ const uid = contextScope[0]?.__mpxProxy?.uid || contextScope[0]?.uid
+ // slots 通过上下文传递,相当于 props
+ // const slots = contextScope[0]?.$slots || {}
+ const slots = contextScope[0]?.slots || {}
+
+ function createEmptyNode () {
+ return createNode('block')
+ }
+
+ function genVnodeTree (node) {
+ if (node.type === 1) {
+ // wxs 模块不需要动态渲染
+ if (node.tag === 'wxs') {
+ return createEmptyNode()
+ } else if (node.for && !node.forProcessed) {
+ return genFor(node)
+ } else if (node.if && !node.ifProcessed) {
+ return genIf(node)
+ } else if (node.tag === 'slot') {
+ return genSlot(node)
+ } else {
+ const data = genData(node)
+ let children = genChildren(node)
+ // 运行时组件的子组件都通过 slots 属性传递
+ if (node.dynamic) {
+ data.slots = resolveSlot(children.slice())
+ children = []
+ }
+ // if (node.dynamic) {
+ // return createDynamicNode(node.aliasTag, data, children)
+ // } else {
+ // return createNode(node.aliasTag || node.tag, data, children)
+ // }
+ return createNode(node.aliasTag || node.tag, data, children)
+ }
+ } else if (node.type === 3) {
+ return genText(node)
+ }
+ }
+
+ function evalExps (exps) {
+ const interpreter = new Interpreter(contextScope)
+ // 消除引用关系
+ let value
+ try {
+ value = interpreter.eval(JSON.parse(JSON.stringify(exps)))
+ } catch (e) {
+ // todo: 后续可以把错误往外抛,业务感知
+ console.warn('the exps is:', exps)
+ console.warn(e)
+ }
+ return value
+ }
+
+ function createNode (tag, data = {}, children = []) {
+ if (Array.isArray(data)) {
+ children = data
+ data = {}
+ }
+ if (typeof tag === 'object') {
+ return tag
+ }
+
+ // 处理 for 循环产生的数组,同时清除空节点
+ children = simpleNormalizeChildren(children).filter(node => !!node?.nt)
+
+ return {
+ nt: tag,
+ d: data,
+ c: children
+ }
+ }
+
+ /**
+ *
+ * 样式隔离的匹配策略优化:
+ *
+ * 条件1: 子组件不能影响到父组件的样式
+ * 条件2: slot 的内容必须在父组件的上下文当中完成样式匹配
+ * 条件3: 匹配过程只能进行一次
+ *
+ * 方案一:根据 moduleId 即作用域来进行匹配
+ * 方案二:根据虚拟树来进行匹配
+ */
+ // function createDynamicNode (moduleId, data = {}, children = []) {
+ // const { template = {}, styles = [] } = staticMap[moduleId]
+ // data.$slots = resolveSlot(children) // 将 slot 通过上下文传递到子组件的渲染流程中
+ // const vnodeTree = _genVnodeTree(template, [data], styles, moduleId)
+ // return vnodeTree
+ // }
+
+ function resolveSlot (children) {
+ const slots = {}
+ if (children.length) {
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ const name = child.data?.slot
+ if (name) {
+ const slot = (slots[name] || (slots[name] = []))
+ if (child.tag === 'template') {
+ slot.push.apply(slot, child.children || [])
+ } else {
+ slot.push(child)
+ }
+ } else {
+ (slots.default || (slots.default = [])).push(child)
+ }
+ }
+ }
+ return slots
+ }
+
+ function genData (node) {
+ const res = {
+ uid,
+ moduleId
+ }
+ if (!node.attrsList) {
+ return res
+ }
+
+ node.attrsList.forEach((attr) => {
+ if (attr.name === 'class' || attr.name === 'style') {
+ // class/style 的表达式为数组形式,class/style的计算过程需要放到逻辑层,主要是因为有逻辑匹配的过程去生成 vnodeTree
+ const helper = attr.name === 'class' ? stringify.stringifyClass : stringify.stringifyStyle
+ let value = ''
+ if (attr.__exps) {
+ const valueArr = attr.__exps.reduce((preVal, curExpression) => {
+ preVal.push(evalExps(curExpression))
+ return preVal
+ }, [])
+ value = helper(...valueArr)
+ } else {
+ value = attr.value
+ }
+ res[attr.name] = value
+ } else if (attr.name === 'data-eventconfigs') {
+ const eventMap = {}
+ attr.__exps?.forEach(({ eventName, exps }) => {
+ eventMap[eventName] = exps.map(exp => evalExps(exp))
+ })
+ res[dash2hump(attr.name)] = eventMap
+ } else {
+ res[dash2hump(attr.name)] = attr.__exps
+ ? evalExps(attr.__exps)
+ : attr.value
+ }
+ })
+
+ return res
+ }
+
+ function genChildren (node) {
+ const res = []
+ const children = node.children || []
+ if (children.length) {
+ children.forEach((item) => {
+ res.push(genNode(item))
+ })
+ }
+ return res
+ }
+
+ function genNode (node) {
+ if (node.type === 1) {
+ return genVnodeTree(node)
+ } else if (node.type === 3 && node.isComment) {
+ return ''
+ // TODO: 注释暂不处理
+ // return _genComment(node)
+ } else {
+ return genText(node) // 文本节点统一通过 _genText 来生成,type = 2(带有表达式的文本,在 mpx 统一处理为了3) || type = 3(纯文本,非注释)
+ }
+ }
+
+ function genText (node) {
+ return {
+ nt: '#text',
+ ct: node.__exps ? evalExps(node.__exps) : node.text
+ }
+ }
+
+ function genFor (node) {
+ node.forProcessed = true
+
+ const itemKey = node.for.item || 'item'
+ const indexKey = node.for.index || 'index'
+ const scope = {
+ [itemKey]: null,
+ [indexKey]: null
+ }
+ const forExp = node.for
+ const res = []
+ let forValue = evalExps(forExp.__exps)
+
+ // 和微信的模版渲染策略保持一致:当 wx:for 的值为字符串时,会将字符串解析成字符串数组
+ if (isString(forValue)) {
+ forValue = forValue.split('')
+ }
+
+ if (Array.isArray(forValue)) {
+ forValue.forEach((item, index) => {
+ // item、index 模板当中如果没申明,需要给到默认值
+ scope[itemKey] = item
+ scope[indexKey] = index
+
+ contextScope.push(scope)
+
+ // 针对 for 循环避免每次都操作的同一个 node 导致数据的污染的问题
+ res.push(deepCloneNode(genVnodeTree(deepCloneNode(node))))
+
+ contextScope.pop()
+ })
+ }
+
+ return res
+ }
+
+ // 对于 if 而言最终生成 <= 1 节点
+ function genIf (node) {
+ if (!node.ifConditions) {
+ node.ifConditions = []
+ return {} // 一个空节点
+ }
+ node.ifProcessed = true
+
+ const ifConditions = node.ifConditions.slice()
+
+ let res = {} // 空节点
+ for (let i = 0; i < ifConditions.length; i++) {
+ const condition = ifConditions[i]
+ // 非 else 节点
+ if (condition.ifExp) {
+ const identifierValue = evalExps(condition.__exps)
+ if (identifierValue) {
+ res = genVnodeTree(condition.block === 'self' ? node : condition.block)
+ break
+ }
+ } else { // else 节点
+ res = genVnodeTree(condition.block)
+ break
+ }
+ }
+ return res
+ }
+
+ // 暂时不支持作用域插槽
+ function genSlot (node) {
+ const data = genData(node) // 计算属性值
+ const slotName = data.name || 'default'
+ return slots[slotName] || null
+ }
+
+ function genVnodeWithStaticCss (vnodeTree) {
+ cssList.forEach((item) => {
+ const [selector, style] = item
+ const nodes = cssSelect(selector, { moduleId })(vnodeTree)
+ nodes?.forEach((node) => {
+ // todo style 合并策略问题:合并过程中缺少了权重关系 style, class 的判断,需要优化
+ node.d.style = node.d.style ? style + node.d.style : style
+ })
+ })
+
+ return vnodeTree
+ }
+
+ const interpreteredVnodeTree = genVnodeTree(vnodeAst)
+
+ return genVnodeWithStaticCss(interpreteredVnodeTree)
+}
\ No newline at end of file
diff --git a/packages/core/src/vnode/staticMap.js b/packages/core/src/vnode/staticMap.js
new file mode 100644
index 0000000000..b1c6ea436a
--- /dev/null
+++ b/packages/core/src/vnode/staticMap.js
@@ -0,0 +1 @@
+export default {}
diff --git a/packages/webpack-plugin/lib/dependencies/RecordTemplateRuntimeInfoDependency.js b/packages/webpack-plugin/lib/dependencies/RecordTemplateRuntimeInfoDependency.js
new file mode 100644
index 0000000000..5057b4b0cf
--- /dev/null
+++ b/packages/webpack-plugin/lib/dependencies/RecordTemplateRuntimeInfoDependency.js
@@ -0,0 +1,93 @@
+const NullDependency = require('webpack/lib/dependencies/NullDependency')
+const makeSerializable = require('webpack/lib/util/makeSerializable')
+
+class RecordTemplateRuntimeInfoDependency extends NullDependency {
+ constructor (packageName, resourcePath, { resourceHashNameMap, runtimeComponents, normalComponents, internalComponents, wxs } = {}) {
+ super()
+ this.packageName = packageName
+ this.resourcePath = resourcePath
+ this.resourceHashNameMap = resourceHashNameMap
+ this.runtimeComponents = runtimeComponents
+ this.normalComponents = normalComponents
+ this.internalComponents = internalComponents
+ this.wxs = wxs
+ }
+
+ get type () {
+ return 'mpx record template runtime info'
+ }
+
+ mpxAction (module, compilation, callback) {
+ const mpx = compilation.__mpx__
+ if (!mpx.runtimeInfo[this.packageName]) {
+ mpx.runtimeInfo[this.packageName] = {
+ resourceHashNameMap: {},
+ internalComponents: {},
+ normalComponents: {
+ 'block': {} // 默认增加block节点,防止根节点渲染失败
+ },
+ runtimeComponents: {},
+ wxs: new Set()
+ }
+ }
+
+ this.mergeResourceHashNameMap(mpx)
+ this.mergeComponentAttrs(mpx)
+ // this.mergeWxs(mpx)
+
+ return callback()
+ }
+
+ // mergeWxs (mpx) {
+ // if (this.wxs.length) {
+ // this.wxs.forEach(item => mpx.runtimeInfo[this.packageName].wxs.add(item))
+ // }
+ // }
+
+ mergeComponentAttrs (mpx) {
+ const componentTypes = ['internalComponents', 'normalComponents', 'runtimeComponents']
+ componentTypes.forEach(type => {
+ const attrsMap = mpx.runtimeInfo[this.packageName][type]
+ for (const tag in this[type]) {
+ if (!attrsMap[tag]) {
+ attrsMap[tag] = {}
+ }
+ Object.assign(attrsMap[tag], this[type][tag])
+ }
+ })
+ }
+
+ mergeResourceHashNameMap (mpx) {
+ Object.assign(mpx.runtimeInfo[this.packageName].resourceHashNameMap, this.resourceHashNameMap)
+ }
+
+ serialize (context) {
+ const { write } = context
+ write(this.packageName)
+ write(this.resourcePath)
+ write(this.resourceHashNameMap)
+ write(this.runtimeComponents)
+ write(this.normalComponents)
+ write(this.internalComponents)
+ super.serialize(context)
+ }
+
+ deserialize (context) {
+ const { read } = context
+ this.packageName = read()
+ this.resourcePath = read()
+ this.resourceHashNameMap = read()
+ this.runtimeComponents = read()
+ this.normalComponents = read()
+ this.internalComponents = read()
+ super.deserialize(context)
+ }
+}
+
+RecordTemplateRuntimeInfoDependency.Template = class RecordModuleTemplateDependencyTemplate {
+ apply () {}
+}
+
+makeSerializable(RecordTemplateRuntimeInfoDependency, '@mpxjs/webpack-plugin/lib/dependencies/RecordTemplateRuntimeInfoDependency')
+
+module.exports = RecordTemplateRuntimeInfoDependency
diff --git a/packages/webpack-plugin/lib/dependencies/RuntimeRenderPackageDependency.js b/packages/webpack-plugin/lib/dependencies/RuntimeRenderPackageDependency.js
new file mode 100644
index 0000000000..4a1f00fd18
--- /dev/null
+++ b/packages/webpack-plugin/lib/dependencies/RuntimeRenderPackageDependency.js
@@ -0,0 +1,39 @@
+const NullDependency = require('webpack/lib/dependencies/NullDependency')
+const makeSerializable = require('webpack/lib/util/makeSerializable')
+
+class RuntimeRenderPackageDependency extends NullDependency {
+ constructor (packageName) {
+ super()
+ this.packageName = packageName
+ }
+
+ get type () {
+ return 'mpx record runtime render package'
+ }
+
+ mpxAction (module, compilation, callback) {
+ const mpx = compilation.__mpx__
+ mpx.usingRuntimePackages.add(this.packageName)
+ return callback()
+ }
+
+ serialize (context) {
+ const { write } = context
+ write(this.packageName)
+ super.serialize(context)
+ }
+
+ deserialize (context) {
+ const { read } = context
+ this.packageName = read()
+ super.deserialize(context)
+ }
+}
+
+RuntimeRenderPackageDependency.Template = class RecordModuleTemplateDependencyTemplate {
+ apply () {}
+}
+
+makeSerializable(RuntimeRenderPackageDependency, '@mpxjs/webpack-plugin/lib/dependencies/RuntimeRenderPackageDependency')
+
+module.exports = RuntimeRenderPackageDependency
diff --git a/packages/webpack-plugin/lib/extractor.js b/packages/webpack-plugin/lib/extractor.js
index 3f4abdaf3c..81a397a8c5 100644
--- a/packages/webpack-plugin/lib/extractor.js
+++ b/packages/webpack-plugin/lib/extractor.js
@@ -5,7 +5,7 @@ const toPosix = require('./utils/to-posix')
const fixRelative = require('./utils/fix-relative')
const addQuery = require('./utils/add-query')
const normalize = require('./utils/normalize')
-const { MPX_DISABLE_EXTRACTOR_CACHE, DEFAULT_RESULT_SOURCE } = require('./utils/const')
+const { MPX_DISABLE_EXTRACTOR_CACHE, DEFAULT_RESULT_SOURCE, DYNAMIC_TEMPLATE, DYNAMIC_STYLE, DYNAMIC, BLOCK_TEMPLATE, BLOCK_STYLES, BLOCK_JSON } = require('./utils/const')
module.exports = content => content
@@ -19,6 +19,7 @@ module.exports.pitch = async function (remainingRequest) {
const issuerResource = queryObj.issuerResource
const fromImport = queryObj.fromImport
const needBabel = queryObj.needBabel
+ const moduleId = queryObj.moduleId || 'm' + mpx.pathHash(resourcePath)
if (needBabel) {
// 创建js request应用babel
@@ -49,6 +50,7 @@ module.exports.pitch = async function (remainingRequest) {
}
let content = await this.importModule(`!!${request}`)
+
// 处理wxss-loader的返回
if (Array.isArray(content)) {
content = content.map((item) => {
@@ -83,11 +85,35 @@ module.exports.pitch = async function (remainingRequest) {
resultSource = assetInfo.extractedResultSource
}
+ let dynamicType = ''
+
+ if (type === BLOCK_TEMPLATE) {
+ dynamicType = DYNAMIC_TEMPLATE
+ }
+ if (type === BLOCK_STYLES) {
+ dynamicType = DYNAMIC_STYLE
+ }
+
+ if (dynamicType && buildInfo.assetsInfo?.get(dynamicType)) {
+ const dynamicAsset = buildInfo.assetsInfo.get(dynamicType).extractedDynamicAsset
+ this.emitFile(DYNAMIC, '', undefined, {
+ skipEmit: true,
+ extractedInfo: {
+ content: dynamicAsset,
+ dynamic: true,
+ type,
+ moduleId,
+ resourcePath,
+ index: 0
+ }
+ })
+ }
+
if (isStatic) {
switch (type) {
// styles为static就两种情况,一种是.mpx中使用src引用样式,第二种为css-loader中处理@import
// 为了支持持久化缓存,.mpx中使用src引用样式对issueFile asset产生的副作用迁移到ExtractDependency中处理
- case 'styles':
+ case BLOCK_STYLES:
if (issuerResource) {
const issuerFile = mpx.getExtractedFile(issuerResource)
let relativePath = toPosix(path.relative(path.dirname(issuerFile), file))
@@ -106,10 +132,10 @@ module.exports.pitch = async function (remainingRequest) {
}
}
break
- case 'template':
+ case BLOCK_TEMPLATE:
resultSource += `module.exports = __webpack_public_path__ + ${JSON.stringify(file)};\n`
break
- case 'json':
+ case BLOCK_JSON:
// 目前json为static时只有处理theme.json一种情况,该情况下返回的路径只能为不带有./或../开头的相对路径,否则微信小程序预览构建会报错,issue#622
resultSource += `module.exports = ${JSON.stringify(file)};\n`
break
diff --git a/packages/webpack-plugin/lib/index.js b/packages/webpack-plugin/lib/index.js
index 53c30f8f57..c9eb2ab22e 100644
--- a/packages/webpack-plugin/lib/index.js
+++ b/packages/webpack-plugin/lib/index.js
@@ -1,6 +1,7 @@
'use strict'
const path = require('path')
+const { AsyncSeriesHook } = require('tapable')
const { ConcatSource, RawSource } = require('webpack').sources
const ResolveDependency = require('./dependencies/ResolveDependency')
const InjectDependency = require('./dependencies/InjectDependency')
@@ -39,7 +40,10 @@ const DynamicEntryDependency = require('./dependencies/DynamicEntryDependency')
const FlagPluginDependency = require('./dependencies/FlagPluginDependency')
const RemoveEntryDependency = require('./dependencies/RemoveEntryDependency')
const RecordVueContentDependency = require('./dependencies/RecordVueContentDependency')
+const RuntimeRenderPackageDependency = require('./dependencies/RuntimeRenderPackageDependency')
+const RecordTemplateRuntimeInfoDependency = require('./dependencies/RecordTemplateRuntimeInfoDependency')
const SplitChunksPlugin = require('webpack/lib/optimize/SplitChunksPlugin')
+const RuntimeRenderPlugin = require('./runtime-render/plugin')
const fixRelative = require('./utils/fix-relative')
const parseRequest = require('./utils/parse-request')
const { matchCondition } = require('./utils/match-condition')
@@ -317,6 +321,7 @@ class MpxWebpackPlugin {
compiler.options.node.global = true
}
+ new RuntimeRenderPlugin().apply(compiler)
const addModePlugin = new AddModePlugin('before-file', this.options.mode, this.options.fileConditionRules, 'file')
const addEnvPlugin = new AddEnvPlugin('before-file', this.options.env, this.options.fileConditionRules, 'file')
const packageEntryPlugin = new PackageEntryPlugin('before-file', this.options.miniNpmPackages, 'file')
@@ -518,7 +523,7 @@ class MpxWebpackPlugin {
}
}
checkDynamicEntryInfo()
- callback()
+ mpx.hooks.finishSubpackagesMake.callAsync(compilation, callback)
})
})
@@ -574,6 +579,12 @@ class MpxWebpackPlugin {
compilation.dependencyFactories.set(RecordVueContentDependency, new NullFactory())
compilation.dependencyTemplates.set(RecordVueContentDependency, new RecordVueContentDependency.Template())
+
+ compilation.dependencyFactories.set(RuntimeRenderPackageDependency, new NullFactory())
+ compilation.dependencyTemplates.set(RuntimeRenderPackageDependency, new RuntimeRenderPackageDependency.Template())
+
+ compilation.dependencyFactories.set(RecordTemplateRuntimeInfoDependency, new NullFactory())
+ compilation.dependencyTemplates.set(RecordTemplateRuntimeInfoDependency, new RecordTemplateRuntimeInfoDependency.Template())
})
compiler.hooks.thisCompilation.tap('MpxWebpackPlugin', (compilation, { normalModuleFactory }) => {
@@ -832,6 +843,9 @@ class MpxWebpackPlugin {
error
})
}
+ },
+ hooks: {
+ finishSubpackagesMake: new AsyncSeriesHook(['compilation'])
}
}
}
@@ -1053,10 +1067,20 @@ class MpxWebpackPlugin {
compilation.hooks.beforeModuleAssets.tap('MpxWebpackPlugin', () => {
const extractedAssetsMap = new Map()
+ // todo 可以通过插件的机制来解耦这部分的逻辑
+ const dynamicAssets = {}
for (const module of compilation.modules) {
const assetsInfo = module.buildInfo.assetsInfo || new Map()
for (const [filename, { extractedInfo } = {}] of assetsInfo) {
if (extractedInfo) {
+ const { moduleId, type, content, dynamic } = extractedInfo
+ if (dynamic) {
+ if (!dynamicAssets[moduleId]) {
+ dynamicAssets[moduleId] = {}
+ }
+ dynamicAssets[moduleId][type] = JSON.parse(content)
+ continue
+ }
let extractedAssets = extractedAssetsMap.get(filename)
if (!extractedAssets) {
extractedAssets = [new Map(), new Map()]
@@ -1068,6 +1092,12 @@ class MpxWebpackPlugin {
}
}
+ if (!isEmptyObject(dynamicAssets)) {
+ const dynamicSource = new RawSource(JSON.stringify(dynamicAssets))
+ // todo 暂时写死输出到一个静态文件,后续看是否通过分包拆分
+ compilation.emitAsset('dynamic.json', dynamicSource)
+ }
+
for (const [filename, [pre, normal]] of extractedAssetsMap) {
const sortedExtractedAssets = [...sortExtractedAssetsMap(pre), ...sortExtractedAssetsMap(normal)]
const source = new ConcatSource()
diff --git a/packages/webpack-plugin/lib/json-compiler/helper.js b/packages/webpack-plugin/lib/json-compiler/helper.js
index 94ae2fd177..694f0ecc3c 100644
--- a/packages/webpack-plugin/lib/json-compiler/helper.js
+++ b/packages/webpack-plugin/lib/json-compiler/helper.js
@@ -99,7 +99,8 @@ module.exports = function createJSONHelper ({ loaderContext, emitWarning, custom
const entry = getDynamicEntry(resource, 'component', outputPath, tarRoot, relativePath)
callback(null, entry, {
tarRoot,
- placeholder
+ placeholder,
+ resourcePath
})
})
}
diff --git a/packages/webpack-plugin/lib/json-compiler/index.js b/packages/webpack-plugin/lib/json-compiler/index.js
index 6f014f21ce..7380ff6194 100644
--- a/packages/webpack-plugin/lib/json-compiler/index.js
+++ b/packages/webpack-plugin/lib/json-compiler/index.js
@@ -14,6 +14,9 @@ const RecordGlobalComponentsDependency = require('../dependencies/RecordGlobalCo
const RecordIndependentDependency = require('../dependencies/RecordIndependentDependency')
const { MPX_DISABLE_EXTRACTOR_CACHE, RESOLVE_IGNORED_ERR, JSON_JS_EXT } = require('../utils/const')
const resolve = require('../utils/resolve')
+const checkIsRuntimeMode = require('../utils/check-is-runtime')
+const isEmptyObject = require('../utils/is-empty-object')
+const resolveMpxCustomElementPath = require('../utils/resolve-mpx-custom-element-path')
const normalize = require('../utils/normalize')
const mpxViewPath = normalize.lib('runtime/components/ali/mpx-view.mpx')
const mpxTextPath = normalize.lib('runtime/components/ali/mpx-text.mpx')
@@ -46,6 +49,7 @@ module.exports = function (content) {
const isApp = !(pagesMap[resourcePath] || componentsMap[resourcePath])
const publicPath = this._compilation.outputOptions.publicPath || ''
const fs = this._compiler.inputFileSystem
+ const runtimeCompile = checkIsRuntimeMode(resourcePath)
const emitWarning = (msg) => {
this.emitWarning(
@@ -172,6 +176,31 @@ module.exports = function (content) {
}
}
+ const fillMpxCustomElement = (isMpxCustomElement = false) => {
+ if (!json.usingComponents) {
+ json.usingComponents = {}
+ }
+ // json.usingComponents = {
+ // element: resolveMpxCustomElementPath(packageName)
+ // }
+ json.usingComponents.element = resolveMpxCustomElementPath(packageName)
+ if (isMpxCustomElement) {
+ Object.assign(json.usingComponents, mpx.getPackageInjectedComponentsMap(packageName))
+ }
+ }
+
+ if (queryObj.mpxCustomElement || runtimeCompile) {
+ // this.cacheable(false)
+ }
+
+ // todo cacheable 的设置
+ if (queryObj.mpxCustomElement) {
+ this.cacheable(false)
+ fillMpxCustomElement(true)
+ callback()
+ return
+ }
+
// 快应用补全json配置,必填项
if (mode === 'qa' && isApp) {
const defaultConf = {
@@ -215,16 +244,55 @@ module.exports = function (content) {
this._module.addPresentationalDependency(new RecordGlobalComponentsDependency(mpx.usingComponents, this.context))
}
+ const runtimeComponentMap = {}
+
+ const collectRuntimeComponents = (name, componentPath) => {
+ if (!isApp && checkIsRuntimeMode(componentPath)) {
+ const moduleId = 'm' + mpx.pathHash(componentPath)
+ runtimeComponentMap[name] = moduleId
+ return moduleId
+ }
+ }
+
+ // todo 后续下掉
+ const injectRuntimeComponents2Script = () => {
+ if (!isEmptyObject(runtimeComponentMap)) {
+ const resultSource = `
+ global.currentInject.getRuntimeModules = function () {
+ return ${JSON.stringify(runtimeComponentMap)}
+ }
+ `
+ this.emitFile(resourcePath, '', undefined, {
+ skipEmit: true,
+ extractedResultSource: resultSource
+ })
+ }
+ }
+
const processComponents = (components, context, callback) => {
if (components) {
async.eachOf(components, (component, name, callback) => {
- processComponent(component, context, { relativePath }, (err, entry, { tarRoot, placeholder } = {}) => {
+ processComponent(component, context, { relativePath }, (err, entry, { tarRoot, placeholder, resourcePath } = {}) => {
if (err === RESOLVE_IGNORED_ERR) {
delete components[name]
return callback()
}
if (err) return callback(err)
components[name] = entry
+ // 可以拿到 resourcePath
+ // 缓存 name, hashName 和 resourcePath,后续在 simplify-template 阶段使用
+ // 运行时组件 usingComponents,能否在 json 生成阶段才去替换 hashName
+ /**
+ * resourcePath: { name }
+ */
+ collectRuntimeComponents(name, resourcePath)
+ // const moduleId = collectRuntimeComponents(name, _resourcePath)
+ // 运行时组件需要 hashName
+ if (runtimeCompile) {
+ const moduleId = 'm' + mpx.pathHash(resourcePath)
+ delete components[name]
+ components[moduleId] = entry
+ }
if (tarRoot) {
if (placeholder) {
placeholder = normalizePlaceholder(placeholder)
@@ -249,7 +317,10 @@ module.exports = function (content) {
callback()
}
})
- }, callback)
+ }, () => {
+ injectRuntimeComponents2Script()
+ callback()
+ })
} else {
callback()
}
@@ -678,6 +749,12 @@ module.exports = function (content) {
},
(callback) => {
processGenerics(json.componentGenerics, this.context, callback)
+ },
+ (callback) => {
+ if (checkIsRuntimeMode(resourcePath)) {
+ fillMpxCustomElement()
+ }
+ callback()
}
], (err) => {
callback(err, processDynamicEntry)
diff --git a/packages/webpack-plugin/lib/loader.js b/packages/webpack-plugin/lib/loader.js
index 360dd55117..1a9278844d 100644
--- a/packages/webpack-plugin/lib/loader.js
+++ b/packages/webpack-plugin/lib/loader.js
@@ -4,6 +4,7 @@ const createHelpers = require('./helpers')
const parseRequest = require('./utils/parse-request')
const { matchCondition } = require('./utils/match-condition')
const addQuery = require('./utils/add-query')
+const checkIsRuntimeMode = require('./utils/check-is-runtime')
const async = require('async')
const processJSON = require('./web/processJSON')
const processScript = require('./web/processScript')
@@ -16,11 +17,14 @@ const AppEntryDependency = require('./dependencies/AppEntryDependency')
const RecordResourceMapDependency = require('./dependencies/RecordResourceMapDependency')
const RecordVueContentDependency = require('./dependencies/RecordVueContentDependency')
const CommonJsVariableDependency = require('./dependencies/CommonJsVariableDependency')
+const RuntimeRenderPackageDependency = require('./dependencies/RuntimeRenderPackageDependency')
const tsWatchRunLoaderFilter = require('./utils/ts-loader-watch-run-loader-filter')
const { MPX_APP_MODULE_ID } = require('./utils/const')
+const resolve = require('./utils/resolve')
const path = require('path')
const processMainScript = require('./web/processMainScript')
const getRulesRunner = require('./platform')
+const isEmptyObject = require('./utils/is-empty-object')
module.exports = function (content) {
this.cacheable()
@@ -50,6 +54,8 @@ module.exports = function (content) {
const localSrcMode = queryObj.mode
const srcMode = localSrcMode || globalSrcMode
const autoScope = matchCondition(resourcePath, mpx.autoScopeRules)
+ const isApp = !(pagesMap[resourcePath] || componentsMap[resourcePath])
+ const isRuntimeMode = checkIsRuntimeMode(resourcePath)
const emitWarning = (msg) => {
this.emitWarning(
@@ -80,6 +86,11 @@ module.exports = function (content) {
const appName = getEntryName(this)
if (appName) this._module.addPresentationalDependency(new AppEntryDependency(resourcePath, appName))
}
+
+ if (isRuntimeMode) {
+ this._module.addPresentationalDependency(new RuntimeRenderPackageDependency(packageName))
+ }
+
const loaderContext = this
const isProduction = this.minimize || process.env.NODE_ENV === 'production'
const filePath = this.resourcePath
@@ -99,6 +110,8 @@ module.exports = function (content) {
let output = ''
const callback = this.async()
+ const componentInfo = {}
+
async.waterfall([
(callback) => {
getJSONContent(parts.json || {}, null, loaderContext, (err, content) => {
@@ -107,6 +120,43 @@ module.exports = function (content) {
callback()
})
},
+ (callback) => {
+ if (!isApp && parts.json && parts.json.content) {
+ const content = JSON5.parse(parts.json.content)
+ const usingComponents = content.usingComponents || {}
+ const usingRuntimeComponents = Object.keys(usingComponents).reduce((preValue, name) => {
+ if (checkIsRuntimeMode(usingComponents[name])) {
+ preValue[name] = usingComponents[name]
+ }
+ return preValue
+ }, {})
+
+ let needMapComponents = null
+ if (!isRuntimeMode) {
+ if (isEmptyObject(usingRuntimeComponents)) {
+ return callback()
+ } else {
+ needMapComponents = usingRuntimeComponents
+ }
+ } else {
+ needMapComponents = usingComponents
+ }
+
+ async.eachOf(needMapComponents, (component, name, callback) => {
+ resolve(loaderContext.context, component, loaderContext, (err, resource) => {
+ if (err) return callback(err)
+ componentInfo[name] = {
+ isRuntimeMode: checkIsRuntimeMode(resource),
+ hashName: 'm' + mpx.pathHash(resource),
+ resourcePath: resource
+ }
+ callback()
+ })
+ }, callback)
+ } else {
+ callback()
+ }
+ },
(callback) => {
const hasScoped = parts.styles.some(({ scoped }) => scoped) || autoScope
const templateAttrs = parts.template && parts.template.attrs
@@ -115,6 +165,7 @@ module.exports = function (content) {
let usingComponents = [].concat(Object.keys(mpx.usingComponents))
let componentPlaceholder = []
+
let componentGenerics = {}
if (parts.json && parts.json.content) {
@@ -300,7 +351,8 @@ module.exports = function (content) {
isNative,
moduleId,
usingComponents,
- componentPlaceholder
+ componentPlaceholder,
+ componentInfo: JSON.stringify(componentInfo)
// 添加babel处理渲染函数中可能包含的...展开运算符
// 由于...运算符应用范围极小以及babel成本极高,先关闭此特性后续看情况打开
// needBabel: true
@@ -336,7 +388,16 @@ module.exports = function (content) {
output += '/* json */\n'
// 给予json默认值, 确保生成json request以自动补全json
const json = parts.json || {}
- output += getRequire('json', json, json.src && { ...queryObj, resourcePath }) + '\n'
+ const extraOptions = {
+ moduleId
+ }
+ if (json.src) {
+ Object.assign(extraOptions, {
+ ...queryObj,
+ resourcePath
+ })
+ }
+ output += getRequire('json', json, extraOptions) + '\n'
// script
output += '/* script */\n'
diff --git a/packages/webpack-plugin/lib/runtime-render/base-wxml.js b/packages/webpack-plugin/lib/runtime-render/base-wxml.js
new file mode 100644
index 0000000000..c0208f4f0e
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/base-wxml.js
@@ -0,0 +1,88 @@
+const mpxConfig = require('../config')
+const { hasExtractAttr } = require('./utils')
+const hash = require('hash-sum')
+
+// todo 节点优化
+const OPTIMIZE_NODES = ['view', 'text', 'image']
+// const OPTIMIZE_NODES = []
+
+let hashIndex = 0
+
+function makeAttrsMap (attrKeys = []) {
+ return attrKeys.reduce((preVal, curVal) => Object.assign(preVal, { [curVal]: '' }), {})
+}
+
+function setCustomEle (el, options, meta) {
+ // 动态组件不需要被收集
+ // if (el.dynamic) return
+ const modeConfig = mpxConfig[options.mode]
+ const directives = new Set([...Object.values(modeConfig.directive), 'slot'])
+ const tag = el.aliasTag || el.tag
+ const attrKeys = Object.keys(el.attrsMap).filter(key => !directives.has(key))
+
+ const eleAttrsMap = el.dynamic ? meta.runtimeInfo.runtimeComponents : meta.runtimeInfo.normalComponents
+ // const eleAttrsMap = meta.runtimeInfo.normalComponents
+ if (tag && !eleAttrsMap[tag]) {
+ eleAttrsMap[tag] = {}
+ if (el.dynamic) {
+ attrKeys.push('slots', 'mpxAttrs')
+ }
+ }
+ Object.assign(eleAttrsMap[tag], makeAttrsMap(attrKeys))
+}
+
+function setBaseEle (el, options, meta) {
+ let aliasTag = ''
+ let hasEvents = false
+ let usingHashTag = false
+ const renderAttrsMap = {}
+ const rawTag = el.tag
+
+ // 属性收集
+ const modeConfig = mpxConfig[options.mode]
+ const directives = new Set([...Object.values(modeConfig.directive), 'slot'])
+ const attrKeys = Object.keys(el.attrsMap).filter(key => !directives.has(key))
+
+ attrKeys.forEach(key => {
+ const eventObj = modeConfig.event.parseEvent(key)
+ if (eventObj) { // 事件的格式化
+ key = `${eventObj.prefix}:${eventObj.eventName}`
+ hasEvents = true
+ // 使用了特殊事件的节点,单独生成一个 hashTag
+ if (['catch', 'capture-bind', 'capture-catch'].includes(eventObj.prefix)) {
+ usingHashTag = true
+ }
+ }
+ renderAttrsMap[key] = ''
+ })
+
+ // 节点类型的优化
+ if (OPTIMIZE_NODES.includes(el.tag) && !hasEvents) {
+ aliasTag = `static-${rawTag}`
+ if (rawTag === 'view' && !hasExtractAttr(el)) {
+ aliasTag = 'pure-view'
+ }
+ }
+
+ if (usingHashTag) {
+ aliasTag = 'd' + hash(`${rawTag}${++hashIndex}`)
+ }
+
+ const tag = aliasTag || rawTag
+
+ if (aliasTag) {
+ renderAttrsMap.rawTag = rawTag
+ el.aliasTag = aliasTag
+ }
+
+ if (!meta.runtimeInfo.internalComponents[tag]) {
+ meta.runtimeInfo.internalComponents[tag] = {}
+ }
+
+ Object.assign(meta.runtimeInfo.internalComponents[tag], renderAttrsMap)
+}
+
+module.exports = function setBaseWxml (el, options, meta) {
+ const set = options.isCustomComponent ? setCustomEle : setBaseEle
+ set(el, options, meta)
+}
diff --git a/packages/webpack-plugin/lib/runtime-render/mpx-custom-element.mpx b/packages/webpack-plugin/lib/runtime-render/mpx-custom-element.mpx
new file mode 100644
index 0000000000..5d6c5958b0
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/mpx-custom-element.mpx
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/webpack-plugin/lib/runtime-render/plugin.js b/packages/webpack-plugin/lib/runtime-render/plugin.js
new file mode 100644
index 0000000000..fb3230fe0e
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/plugin.js
@@ -0,0 +1,72 @@
+const path = require('path')
+const async = require('async')
+const toPosix = require('../utils/to-posix')
+
+const MPX_CUSTOM_ELEMENT = 'mpx-custom-element'
+
+const processMpxCustomElement = (mpx, packageName, callback) => {
+ let outputPath = `${MPX_CUSTOM_ELEMENT}-${packageName}`
+ if (packageName !== 'main') {
+ outputPath = toPosix(path.join(packageName, outputPath))
+ }
+ const elementPath = path.resolve(__dirname, 'mpx-custom-element.mpx')
+ if (!mpx.componentsMap[packageName]) {
+ return callback()
+ }
+ // 挂载组件信息至 componentsMap
+ mpx.componentsMap[packageName][elementPath] = outputPath
+ // 添加自定义组件进入编译流程
+ mpx.addEntry(elementPath + `?mpxCustomElement&isComponent&packageRoot=${packageName}`, outputPath, (err, module) => {
+ // 自定义容器组件不缓存
+ module.invalidateBuild()
+ if (err) return callback(err)
+ callback()
+ })
+}
+
+module.exports = class RuntimeRenderPlugin {
+ apply (compiler) {
+ compiler.hooks.thisCompilation.tap({
+ name: 'RuntimeRenderPlugin',
+ stage: 1000
+ }, (compilation) => {
+ if (compilation.__mpx__) {
+ const mpx = compilation.__mpx__
+
+ // 使用了运行时渲染的 package
+ mpx.usingRuntimePackages = new Set()
+ // 以包为维度记录不同 package 需要的组件属性等信息,用以最终 mpx-custom-element 相关文件的输出
+ mpx.runtimeInfo = {}
+
+ // 注入到 mpx-custom-element-*.json 里面的组件路径
+ mpx.getPackageInjectedComponentsMap = function (packageName = 'main') {
+ const res = {}
+ const componentsMap = Object.values(mpx.componentsMap).reduce((preVal, curVal) => Object.assign(preVal, curVal), {})
+ const resourceHashNameMap = mpx.runtimeInfo[packageName].resourceHashNameMap
+ const outputPath = compilation.outputOptions.publicPath || ''
+ for (const path in resourceHashNameMap) {
+ const hashName = resourceHashNameMap[path]
+ if (hashName && componentsMap[path]) {
+ res[hashName] = outputPath + componentsMap[path]
+ }
+ }
+
+ return res
+ }
+
+ mpx.hooks.finishSubpackagesMake.tapAsync('MpxCustomElementEntry', (compilation, callback) => {
+ if (mpx.usingRuntimePackages.size === 0) {
+ return callback()
+ }
+
+ const tasks = Array.from(mpx.usingRuntimePackages).map(pkg => (callback) => {
+ processMpxCustomElement(mpx, pkg, callback)
+ })
+ async.parallel(tasks, () => {
+ callback()
+ })
+ })
+ }
+ })
+ }
+}
diff --git a/packages/webpack-plugin/lib/runtime-render/template.js b/packages/webpack-plugin/lib/runtime-render/template.js
new file mode 100644
index 0000000000..99355282ab
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/template.js
@@ -0,0 +1,16 @@
+const {
+ UnRecursiveTemplate,
+ RecursiveTemplate
+} = require('@mpxjs/template-engine')
+
+const recursiveTemplate = new RecursiveTemplate()
+const unRecursiveTemplate = new UnRecursiveTemplate()
+
+module.exports.buildTemplate = function (mode, config) {
+ const isAli = mode === 'ali'
+ const template = isAli ? recursiveTemplate : unRecursiveTemplate
+ return template.buildTemplate({
+ ...config,
+ inlineSlot: isAli
+ })
+}
diff --git a/packages/webpack-plugin/lib/runtime-render/utils.js b/packages/webpack-plugin/lib/runtime-render/utils.js
new file mode 100644
index 0000000000..7aea49ea46
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/utils.js
@@ -0,0 +1,50 @@
+const isEmptyObject = require('../utils/is-empty-object')
+const config = require('../config')
+
+const directiveSet = new Set()
+const commonBaseAttrs = ['class', 'style', 'id', 'hidden']
+const commonMpxAttrs = ['mpxShow', 'slots']
+
+function genRegExp (arrStr) {
+ return new RegExp(`^(${arrStr.join('|')})$`)
+}
+
+module.exports = {
+ transformSlotsToString (slotsMap = {}) {
+ let res = '{'
+ if (!isEmptyObject(slotsMap)) {
+ Object.keys(slotsMap).forEach((slotTarget) => {
+ res += `${slotTarget}: [`
+ const renderFns = slotsMap[slotTarget] || []
+ renderFns.forEach((renderFn) => {
+ if (renderFn) {
+ res += `${renderFn},`
+ }
+ })
+ res += '],'
+ })
+ }
+ res += '}'
+ return res
+ },
+ hasExtractAttr (el) {
+ const res = Object.keys(el.attrsMap).find(attr => {
+ return !(genRegExp(commonBaseAttrs).test(attr) || attr.startsWith('data-'))
+ })
+ return Boolean(res)
+ },
+ isCommonAttr (attr) {
+ return genRegExp([...commonBaseAttrs, ...commonMpxAttrs]).test(attr) || attr.startsWith('data-')
+ },
+ isDirective (key) {
+ return directiveSet.has(key)
+ },
+ updateModeDirectiveSet (mode) {
+ const directiveMap = config[mode].directive
+ if (!isEmptyObject(directiveMap)) {
+ for (const key in directiveMap) {
+ directiveSet.add(directiveMap[key])
+ }
+ }
+ }
+}
diff --git a/packages/webpack-plugin/lib/style-compiler/index.js b/packages/webpack-plugin/lib/style-compiler/index.js
index df1ba90bc6..2ca93b3fe0 100644
--- a/packages/webpack-plugin/lib/style-compiler/index.js
+++ b/packages/webpack-plugin/lib/style-compiler/index.js
@@ -1,14 +1,16 @@
const path = require('path')
const postcss = require('postcss')
const loadPostcssConfig = require('./load-postcss-config')
-const { MPX_ROOT_VIEW, MPX_APP_MODULE_ID } = require('../utils/const')
+const { MPX_ROOT_VIEW, MPX_APP_MODULE_ID, DYNAMIC_STYLE } = require('../utils/const')
const rpx = require('./plugins/rpx')
const vw = require('./plugins/vw')
const pluginCondStrip = require('./plugins/conditional-strip')
const scopeId = require('./plugins/scope-id')
const transSpecial = require('./plugins/trans-special')
+const cssArrayList = require('./plugins/css-array-list')
const { matchCondition } = require('../utils/match-condition')
const parseRequest = require('../utils/parse-request')
+const checkIsRuntimeMode = require('../utils/check-is-runtime')
module.exports = function (css, map) {
this.cacheable()
@@ -22,6 +24,7 @@ module.exports = function (css, map) {
const isApp = resourcePath === appInfo.resourcePath
const transRpxRulesRaw = mpx.transRpxRules
const transRpxRules = transRpxRulesRaw ? (Array.isArray(transRpxRulesRaw) ? transRpxRulesRaw : [transRpxRulesRaw]) : []
+ const runtimeCompile = checkIsRuntimeMode(resourcePath)
const transRpxFn = mpx.webConfig.transRpxFn
const testResolveRange = (include = () => true, exclude) => {
@@ -85,6 +88,11 @@ module.exports = function (css, map) {
const finalPlugins = config.prePlugins.concat(plugins, config.plugins)
+ const cssList = []
+ if (runtimeCompile) {
+ finalPlugins.push(cssArrayList(cssList))
+ }
+
return postcss(finalPlugins)
.process(css, options)
.then(result => {
@@ -128,6 +136,14 @@ module.exports = function (css, map) {
}
}
+ if (runtimeCompile) {
+ this.emitFile(DYNAMIC_STYLE, '', undefined, {
+ skipEmit: true,
+ extractedDynamicAsset: JSON.stringify(cssList)
+ })
+ return cb(null, '')
+ }
+
const map = result.map && result.map.toJSON()
cb(null, result.css, map)
return null // silence bluebird warning
diff --git a/packages/webpack-plugin/lib/style-compiler/plugins/css-array-list.js b/packages/webpack-plugin/lib/style-compiler/plugins/css-array-list.js
new file mode 100644
index 0000000000..a37787d7a8
--- /dev/null
+++ b/packages/webpack-plugin/lib/style-compiler/plugins/css-array-list.js
@@ -0,0 +1,26 @@
+module.exports = (cssList = []) => {
+ // Work with options here
+
+ return {
+ postcssPlugin: 'css-array-list',
+ /*
+ Root (root, postcss) {
+ // Transform CSS AST here
+ }
+ */
+
+ Rule (rule) {
+ // todo 特殊字符的处理,vtree 内部是否有做处理
+ const selector = rule.selector.trim().replace('\n', '')
+ let decls = ''
+ if (rule.nodes && rule.nodes.length) {
+ rule.nodes.forEach(item => {
+ decls += `${item.prop}: ${item.value}; `
+ })
+ }
+ cssList.push([selector, decls])
+ }
+ }
+}
+
+module.exports.postcss = true
diff --git a/packages/webpack-plugin/lib/template-compiler/compiler.js b/packages/webpack-plugin/lib/template-compiler/compiler.js
index 9a9870e48c..7ee664e790 100644
--- a/packages/webpack-plugin/lib/template-compiler/compiler.js
+++ b/packages/webpack-plugin/lib/template-compiler/compiler.js
@@ -12,6 +12,8 @@ const transDynamicClassExpr = require('./trans-dynamic-class-expr')
const dash2hump = require('../utils/hump-dash').dash2hump
const makeMap = require('../utils/make-map')
const { isNonPhrasingTag } = require('../utils/dom-tag-config')
+const setBaseWxml = require('../runtime-render/base-wxml')
+const { capitalToHyphen } = require('../utils/string')
const no = function () {
return false
@@ -670,6 +672,7 @@ function parse (template, options) {
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
const element = createASTElement(tag, attrs, currentParent)
+
if (ns) {
element.ns = ns
}
@@ -1141,7 +1144,8 @@ function processBindEvent (el, options) {
if (!isEmptyObject(eventConfigMap)) {
addAttrs(el, [{
name: 'data-eventconfigs',
- value: `{{${config[mode].event.shallowStringify(eventConfigMap)}}}`
+ value: `{{${config[mode].event.shallowStringify(eventConfigMap)}}}`,
+ eventConfigMap
}])
}
}
@@ -1154,7 +1158,7 @@ function parseMustacheWithContext (raw = '') {
return parseMustache(raw, (exp) => {
if (defs) {
// eval处理的话,和别的判断条件,比如运行时的判断混用情况下得不到一个结果,还是正则替换
- const defKeys = Object.keys(defs)
+ const defKeys = Object.keys(defs || {})
defKeys.forEach((defKey) => {
const defRE = new RegExp(`\\b${defKey}\\b`)
const defREG = new RegExp(`\\b${defKey}\\b`, 'g')
@@ -1609,7 +1613,9 @@ function processClass (el, meta) {
addAttrs(el, [{
name: targetType,
// swan中externalClass是通过编译时静态实现,因此需要保留原有的staticClass形式避免externalClass失效
- value: mode === 'swan' && staticClass ? `${staticClass} {{${stringifyModuleName}.stringifyClass('', ${dynamicClassExp})}}` : `{{${stringifyModuleName}.stringifyClass(${staticClassExp}, ${dynamicClassExp})}}`
+ value: mode === 'swan' && staticClass ? `${staticClass} {{${stringifyModuleName}.stringifyClass('', ${dynamicClassExp})}}` : `{{${stringifyModuleName}.stringifyClass(${staticClassExp}, ${dynamicClassExp})}}`,
+ staticClassExp,
+ dynamicClassExp
}])
injectWxs(meta, stringifyModuleName, stringifyWxsPath)
} else if (staticClass) {
@@ -1642,7 +1648,9 @@ function processStyle (el, meta) {
const dynamicStyleExp = parseMustacheWithContext(dynamicStyle).result
addAttrs(el, [{
name: targetType,
- value: `{{${stringifyModuleName}.stringifyStyle(${staticStyleExp}, ${dynamicStyleExp})}}`
+ value: `{{${stringifyModuleName}.stringifyStyle(${staticStyleExp}, ${dynamicStyleExp})}}`,
+ staticStyleExp,
+ dynamicStyleExp
}])
injectWxs(meta, stringifyModuleName, stringifyWxsPath)
} else if (staticStyle) {
@@ -1665,6 +1673,10 @@ function isComponentNode (el, options) {
return options.usingComponents.indexOf(el.tag) !== -1 || el.tag === 'component'
}
+function isRuntimeComponentNode (el, options) {
+ return !!(options.componentInfo && options.componentInfo[el.tag] && options.componentInfo[el.tag].isRuntimeMode)
+}
+
function processAliExternalClassesHack (el, options) {
const isComponent = isComponentNode(el, options)
// 处理组件externalClass多层传递
@@ -2042,6 +2054,13 @@ function processDuplicateAttrsList (el) {
el.attrsList = attrsList
}
+function processRuntime (el, options) {
+ const isDynamic = isRuntimeComponentNode(el, options)
+ if (isDynamic) {
+ el.dynamic = isDynamic
+ }
+}
+
// 处理wxs注入逻辑
function processInjectWxs (el, meta) {
if (el.injectWxsProps && el.injectWxsProps.length) {
@@ -2068,6 +2087,7 @@ function processMpxTagName (el) {
}
function processElement (el, root, options, meta) {
+ processRuntime(el, options)
processAtMode(el)
// 如果已经标记了这个元素要被清除,直接return跳过后续处理步骤
if (el._atModeStatus === 'mismatch') {
@@ -2130,6 +2150,8 @@ function processElement (el, root, options, meta) {
function closeElement (el, meta, options) {
postProcessAtMode(el)
+ postProcessRuntime(el, options, meta)
+
if (mode === 'web') {
postProcessWxs(el, meta)
// 处理代码维度条件编译移除死分支
@@ -2150,6 +2172,72 @@ function closeElement (el, meta, options) {
postProcessIf(el)
}
+// 部分节点类型不需要被收集
+const RUNTIME_FILTER_NODES = ['import', 'template', 'wxs', 'component', 'slot']
+
+// 节点收集,最终注入到 mpx-custom-element-*.wxml 中
+function postProcessRuntime (el, options, meta) {
+ if (RUNTIME_FILTER_NODES.includes(el.tag)) {
+ return
+ }
+ const isCustomComponent = isComponentNode(el, options)
+
+ // todo 下掉
+ // 非运行时组件/页面当中使用了运行时组件,使用 if block 包裹
+ if (!options.runtimeCompile && el.dynamic) {
+ addIfBlock(el, '__mpxDynamicLoaded')
+ }
+
+ // 运行时的组件收集节点信息
+ if (options.runtimeCompile) {
+ if (!meta.runtimeInfo) {
+ meta.runtimeInfo = {
+ // resourcePath: {
+ // baseNodes: {},
+ // customNodes: {}
+ // },
+ resourceHashNameMap: {},
+ internalComponents: {},
+ runtimeComponents: {},
+ normalComponents: {},
+ wxs: {}
+ }
+ }
+
+ const tag = Object.keys(options.componentInfo).find((key) => {
+ if (mode === 'ali' || mode === 'swan') {
+ return capitalToHyphen(key) === el.tag
+ }
+ return key === el.tag
+ })
+ const componentInfo = options.componentInfo[tag]
+ if (isCustomComponent && componentInfo) {
+ const { hashName, resourcePath } = componentInfo
+ el.aliasTag = hashName
+ meta.runtimeInfo.resourceHashNameMap[resourcePath] = hashName
+ }
+
+ // 按需收集节点属性信息,存储到 meta 后到外层处理
+ setBaseWxml(el, { mode, isCustomComponent }, meta)
+ }
+}
+
+function addIfBlock (el, ifCondition) {
+ const blockNode = createASTElement('block', [{
+ name: config[mode].directive.if,
+ value: `{{ ${ifCondition} }}`
+ }], el.parent)
+ blockNode.if = {
+ raw: `{{ ${ifCondition} }}`,
+ exp: ifCondition
+ }
+ const nodeIndex = el.parent.children.findIndex(item => item === el)
+ const oldParent = el.parent
+ el.parent = blockNode
+ blockNode.children.push(el)
+ oldParent.children.splice(nodeIndex, 1, blockNode)
+}
+
function postProcessAtMode (el) {
if (el._atModeStatus === 'mismatch') {
removeNode(el, true)
@@ -2574,5 +2662,10 @@ module.exports = {
parseMustache,
parseMustacheWithContext,
stringifyWithResolveComputed,
- addAttrs
+ addAttrs,
+ getAndRemoveAttr,
+ findPrevNode,
+ removeNode,
+ replaceNode,
+ createASTElement
}
diff --git a/packages/webpack-plugin/lib/template-compiler/index.js b/packages/webpack-plugin/lib/template-compiler/index.js
index c0cc729daa..9ca77806f2 100644
--- a/packages/webpack-plugin/lib/template-compiler/index.js
+++ b/packages/webpack-plugin/lib/template-compiler/index.js
@@ -3,6 +3,12 @@ const bindThis = require('./bind-this')
const parseRequest = require('../utils/parse-request')
const { matchCondition } = require('../utils/match-condition')
const loaderUtils = require('loader-utils')
+const checkIsRuntimeMode = require('../utils/check-is-runtime')
+const { MPX_DISABLE_EXTRACTOR_CACHE, DYNAMIC_TEMPLATE } = require('../utils/const')
+const RecordTemplateRuntimeInfoDependency = require('../dependencies/RecordTemplateRuntimeInfoDependency')
+const simplifyAstTemplate = require('./simplify-template')
+
+const { buildTemplate } = require('../runtime-render/template')
module.exports = function (raw) {
this.cacheable()
@@ -28,6 +34,9 @@ module.exports = function (raw) {
const isNative = queryObj.isNative
const hasScoped = queryObj.hasScoped
const moduleId = queryObj.moduleId || 'm' + mpx.pathHash(resourcePath)
+ const runtimeCompile = checkIsRuntimeMode(resourcePath)
+ const componentInfo = JSON.parse(queryObj.componentInfo || '{}')
+ const moduleIdString = JSON.stringify(moduleId)
let optimizeRenderLevel = 0
for (const rule of optimizeRenderRules) {
@@ -48,9 +57,17 @@ module.exports = function (raw) {
)
}
+ if (queryObj.mpxCustomElement) {
+ this.cacheable(false)
+ raw = '\n'
+ raw += buildTemplate(mode, mpx.runtimeInfo[packageName])
+ }
+
const { root: ast, meta } = compiler.parse(raw, {
warn,
error,
+ componentInfo,
+ runtimeCompile,
usingComponents,
componentPlaceholder,
hasComment,
@@ -70,10 +87,21 @@ module.exports = function (raw) {
i18n,
checkUsingComponents: matchCondition(resourcePath, mpx.checkUsingComponentsRules),
globalComponents: Object.keys(mpx.usingComponents),
- forceProxyEvent: matchCondition(resourcePath, mpx.forceProxyEventRules),
+ forceProxyEvent: matchCondition(resourcePath, mpx.forceProxyEventRules) || runtimeCompile,
hasVirtualHost: matchCondition(resourcePath, mpx.autoVirtualHostRules)
})
+ if (meta.runtimeInfo || runtimeCompile) {
+ // if (meta.wxsModuleMap) {
+ // meta.runtimeInfo.wxs = Object.keys(meta.wxsModuleMap)
+ // }
+ // 包含了运行时组件的template模块必须每次都创建(但并不是每次都需要build),用于收集组件节点信息,传递信息以禁用父级extractor的缓存
+ this.emitFile(MPX_DISABLE_EXTRACTOR_CACHE, '', undefined, { skipEmit: true })
+ // 以 package 为维度存储,meta 上的数据也只是存储了这个组件的 template 上获取的信息,需要在 dependency 里面再次进行合并操作
+ this._module.addPresentationalDependency(new RecordTemplateRuntimeInfoDependency(packageName, resourcePath, meta.runtimeInfo))
+ this.cacheable(false)
+ }
+
if (meta.wxsContentMap) {
for (const module in meta.wxsContentMap) {
wxsContentMap[`${resourcePath}~${module}`] = meta.wxsContentMap[module]
@@ -89,43 +117,50 @@ module.exports = function (raw) {
const result = compiler.serialize(ast)
- if (isNative) {
+ if (isNative || queryObj.mpxCustomElement) {
return result
}
resultSource += `
global.currentInject = {
- moduleId: ${JSON.stringify(moduleId)}
+ moduleId: ${moduleIdString}
};\n`
- const rawCode = compiler.genNode(ast)
- if (rawCode) {
- try {
- const ignoreMap = Object.assign({
- _i: true,
- _c: true,
- _sc: true,
- _r: true
- }, meta.wxsModuleMap)
- const bindResult = optimizeRenderLevel === 2
- ? bindThis.transformSimple(rawCode, {
- ignoreMap
- })
- : bindThis.transform(rawCode, {
- needCollect: true,
- renderReduce: optimizeRenderLevel === 1,
- ignoreMap
- })
- resultSource += `
+ if (runtimeCompile) {
+resultSource += `
+global.currentInject.render = function(_i, _c, _r, _sc, _g) {
+ _r(false, _g(${moduleIdString}))
+}
+`
+ } else {
+ const rawCode = compiler.genNode(ast)
+ if (rawCode) {
+ try {
+ const ignoreMap = Object.assign({
+ _i: true,
+ _c: true,
+ _sc: true,
+ _r: true
+ }, meta.wxsModuleMap)
+ const bindResult = optimizeRenderLevel === 2
+ ? bindThis.transformSimple(rawCode, {
+ ignoreMap
+ })
+ : bindThis.transform(rawCode, {
+ needCollect: true,
+ renderReduce: optimizeRenderLevel === 1,
+ ignoreMap
+ })
+ resultSource += `
global.currentInject.render = function (_i, _c, _r, _sc) {
${bindResult.code}
_r(${optimizeRenderLevel === 2 ? 'true' : ''});
};\n`
- if ((mode === 'tt' || mode === 'swan') && bindResult.propKeys) {
- resultSource += `global.currentInject.propKeys = ${JSON.stringify(bindResult.propKeys)};\n`
- }
- } catch (e) {
- error(`
+ if ((mode === 'tt' || mode === 'swan') && bindResult.propKeys) {
+ resultSource += `global.currentInject.propKeys = ${JSON.stringify(bindResult.propKeys)};\n`
+ }
+ } catch (e) {
+ error(`
Invalid render function generated by the template, please check!\n
Template result:
${result}\n
@@ -133,7 +168,8 @@ Error code:
${rawCode}
Error Detail:
${e.stack}`)
- return result
+ return result
+ }
}
}
@@ -160,5 +196,21 @@ global.currentInject.getRefsData = function () {
extractedResultSource: resultSource
})
+ // 运行时编译的组件直接返回基础模板的内容,并产出动态文本内容
+ if (runtimeCompile) {
+ let simpleAst = ''
+ try {
+ simpleAst = simplifyAstTemplate(ast, mode)
+ } catch (e) {
+ error(`simplify the runtime component ast node fail, please check!\n Error Detail: ${e.stack}`)
+ }
+ this.emitFile(DYNAMIC_TEMPLATE, '', undefined, {
+ skipEmit: true,
+ extractedDynamicAsset: JSON.stringify(simpleAst)
+ })
+ return ''
+ // return ''
+ }
+
return result
}
diff --git a/packages/webpack-plugin/lib/template-compiler/parse-exps.js b/packages/webpack-plugin/lib/template-compiler/parse-exps.js
new file mode 100644
index 0000000000..66a6abda8e
--- /dev/null
+++ b/packages/webpack-plugin/lib/template-compiler/parse-exps.js
@@ -0,0 +1,148 @@
+// todo 待讨论 parser + interpreter 的部分是否需要单独抽个 package 出去
+const acorn = require('acorn')
+const walk = require('acorn-walk')
+
+/**
+ * 基于目前小程序所支持的模版语法实现,对于不支持的语法在编译阶段直接报错
+ */
+
+const NODE_TYPE = {
+ Program: 1,
+ Identifier: 2,
+ Literal: 3,
+ ArrayExpression: 28,
+ ObjectExpression: 29,
+ Property: 31,
+ UnaryExpression: 33,
+ // UpdateExpression: 34,
+ BinaryExpression: 35,
+ LogicalExpression: 37,
+ MemberExpression: 38,
+ ConditionalExpression: 39,
+ ExpressionStatement: 40
+}
+
+const error = function (msg) {
+ throw new Error(`[Mpx dynamic expression parser error]: ${msg}`)
+}
+
+walk.full = function full (node, baseVisitor, state, override) {
+ const stack = []
+ ; (function c (node, st, override, s) {
+ const type = override || node.type
+ if (!baseVisitor[type]) {
+ error(`${type} grammar is not supported in the template`)
+ }
+ baseVisitor[type](node, st, c, s)
+ })(node, state, override, stack)
+
+ // 限定 bodyStack 长度,仅支持单表达式的写法
+ const bodyStackIndex = 1
+ if (stack[bodyStackIndex].length > 1) {
+ error('only support one expression in the template')
+ }
+ return stack
+}
+
+const baseVisitor = {}
+
+baseVisitor.UnaryExpression = function (node, st, c, s) {
+ // const nodeType = node.type === 'UnaryExpression' ? NODE_TYPE.UnaryExpression : NODE_TYPE.UpdateExpression
+ const nodeType = NODE_TYPE.UnaryExpression
+ const argumentNodeStack = []
+ s.push(nodeType, node.operator, argumentNodeStack, node.prefix)
+ c(node.argument, st, 'Expression', argumentNodeStack)
+}
+
+baseVisitor.BinaryExpression = baseVisitor.LogicalExpression = function (node, st, c, s) {
+ const nodeType = node.type === 'BinaryExpression' ? NODE_TYPE.BinaryExpression : NODE_TYPE.LogicalExpression
+ const leftNodeStack = []
+ const rightNodeStack = []
+ // todo operator 可以严格按照小程序模版能力进行限制
+ s.push(nodeType, node.operator, leftNodeStack, rightNodeStack)
+ c(node.left, st, 'Expression', leftNodeStack)
+ c(node.right, st, 'Expression', rightNodeStack)
+}
+
+baseVisitor.ConditionalExpression = (node, st, c, s) => {
+ const testNodeStack = []
+ const consequentNodeStack = []
+ const alternateNodeStack = []
+ s.push(NODE_TYPE.ConditionalExpression, testNodeStack, consequentNodeStack, alternateNodeStack)
+ c(node.test, st, 'Expression', testNodeStack)
+ c(node.consequent, st, 'Expression', consequentNodeStack)
+ c(node.alternate, st, 'Expression', alternateNodeStack)
+}
+
+const visitor = walk.make({
+ Program (node, st, c, s) {
+ const bodyStack = []
+ s.push(NODE_TYPE.Program, bodyStack)
+ for (let i = 0, list = node.body; i < list.length; i += 1) {
+ const stmt = list[i]
+ bodyStack[i] = []
+ c(stmt, st, 'Statement', bodyStack[i])
+ }
+ },
+ ExpressionStatement (node, st, c, s) {
+ const expressionStack = []
+ s.push(NODE_TYPE.ExpressionStatement, expressionStack)
+ c(node.expression, st, null, expressionStack)
+ },
+ MemberExpression (node, st, c, s) {
+ const objectNodeStack = []
+ const propertyNodeStack = []
+ s.push(NODE_TYPE.MemberExpression, objectNodeStack, propertyNodeStack, node.computed)
+ c(node.object, st, 'Expression', objectNodeStack)
+ c(node.property, st, 'Expression', propertyNodeStack)
+ },
+ ArrayExpression (node, st, c, s) {
+ const elementsStack = []
+ s.push(NODE_TYPE.ArrayExpression, elementsStack)
+ node.elements.forEach((elt, index) => {
+ if (elt) {
+ elementsStack[index] = []
+ c(elt, st, 'Expression', elementsStack[index])
+ }
+ })
+ },
+ ObjectExpression (node, st, c, s) {
+ const propertiesStack = []
+ s.push(NODE_TYPE.ObjectExpression, propertiesStack)
+ node.properties.forEach((prop, index) => {
+ propertiesStack[index] = []
+ c(prop, st, null, propertiesStack[index])
+ })
+ },
+ Property (node, st, c, s) {
+ const keyNodeStack = []
+ const valueNodeStack = []
+ s.push(NODE_TYPE.Property, keyNodeStack, valueNodeStack, node.kind)
+ c(node.key, st, 'Expression', keyNodeStack)
+ c(node.value, st, 'Expression', valueNodeStack)
+ },
+ Literal (node, st, c, s) {
+ // todo node.raw/-1 目前应该都用不到,后续可以优化
+ s.push(NODE_TYPE.Literal, node.value, node.raw, -1) // -1?
+ },
+ Identifier (node, st, c, s) {
+ s.push(NODE_TYPE.Identifier, node.name)
+ },
+ Expression (node, st, c, s) {
+ c(node, st, null, s)
+ },
+ Statement (node, st, c, s) {
+ c(node, st, null, s)
+ }
+}, baseVisitor)
+
+module.exports = {
+ parseExp (str) {
+ // 确保 str 都是为 expressionStatement
+ if (!/^\(*\)$/.test(str)) {
+ str = `(${str})`
+ }
+ return walk.full(acorn.parse(str, { ecmaVersion: 'es5' }), visitor)
+ },
+ NODE_TYPE
+}
diff --git a/packages/webpack-plugin/lib/template-compiler/simplify-template.js b/packages/webpack-plugin/lib/template-compiler/simplify-template.js
new file mode 100644
index 0000000000..637405e9d0
--- /dev/null
+++ b/packages/webpack-plugin/lib/template-compiler/simplify-template.js
@@ -0,0 +1,273 @@
+const { getAndRemoveAttr, parseMustache, findPrevNode, replaceNode, createASTElement } = require('./compiler')
+const allConfigs = require('../config')
+const { parseExp } = require('./parse-exps')
+
+function processIf (vnode, config) {
+ delete vnode.ifProcessed
+
+ if (vnode.if) {
+ getAndRemoveAttr(vnode, config.directive.if)
+ const parsedExp = vnode.if.exp
+ addIfCondition(vnode, {
+ ifExp: true,
+ block: 'self',
+ __exps: parseExp(parsedExp)
+ })
+
+ vnode.if = true
+ } else if (vnode.elseif || vnode.else) {
+ const directive = vnode.elseif ? config.directive.elseif : config.directive.else
+ getAndRemoveAttr(vnode, directive)
+ processIfConditions(vnode)
+
+ delete vnode.elseif
+ delete vnode.else
+ } else if (typeof vnode._if === 'boolean') {
+ // 如果节点有 _if 属性,那么其值为一个常量值
+ // 如果值为 true,一定会渲染这一个节点,当成一个普通节点即可,因为编译阶段已经 delete if
+ if (vnode._if === true) {
+ // do nothing
+ }
+
+ // 如果值为 false,后续的遍历过程会删除这个节点,本来也不需要被渲染出来
+ if (vnode._if === false) {
+ // do nothing
+ }
+ }
+}
+
+function addIfCondition (el, condition) {
+ if (!el.ifConditions) {
+ el.ifConditions = []
+ }
+ el.ifConditions.push(condition)
+}
+
+function processIfConditions (el) {
+ const prev = findPrevIfNode(el)
+ if (prev) {
+ addIfCondition(prev, {
+ ifExp: !!el.elseif,
+ block: el,
+ __exps: el.elseif ? parseExp(el.elseif.exp) : ''
+ })
+
+ const tempNode = createASTElement('block', [])
+ tempNode._tempIf = true // 创建一个临时的节点,后续遍历会删除
+ replaceNode(el, tempNode)
+ }
+}
+
+function findPrevIfNode (el) {
+ const prevNode = findPrevNode(el)
+ if (!prevNode) {
+ return null
+ }
+
+ if (prevNode._tempIf) {
+ return findPrevIfNode(prevNode)
+ } else if (prevNode.if) {
+ return prevNode
+ } else {
+ return null
+ }
+}
+
+function processFor (vnode) {
+ if (vnode.for) {
+ vnode.for.__exps = parseExp(vnode.for.exp)
+
+ delete vnode.for.raw
+ delete vnode.for.exp
+ }
+}
+
+function processAttrsMap (vnode, config) {
+ processDirectives(vnode, config)
+
+ if (vnode.attrsList && vnode.attrsList.length) {
+ for (let i = vnode.attrsList.length - 1; i >= 0; i--) {
+ const attr = vnode.attrsList[i]
+ if (attr.name === 'class') {
+ processClass(attr)
+ } else if (attr.name === 'style') {
+ processStyle(attr)
+ } else if (attr.name === 'data-eventconfigs') {
+ processBindEvent(attr)
+ } else if (config.event.parseEvent(attr.name)) { // 原本的事件代理直接剔除,主要是基础模版的事件直接走代理形式,事件绑定名直接写死的
+ vnode.attrsList.splice(i, 1)
+ } else {
+ const exps = getAttrExps(attr)
+ if (exps) {
+ attr.__exps = exps
+ }
+ }
+
+ if (attr.__exps) {
+ delete attr.value
+ }
+ }
+ } else {
+ // 如果长度为空,ast 产出物可以不输出
+ delete vnode.attrsList
+ }
+
+ delete vnode.attrsMap
+}
+
+function processClass (attr) {
+ const { staticClassExp = '', dynamicClassExp = '' } = attr
+ if (staticClassExp || dynamicClassExp) {
+ attr.__exps = [parseExp(staticClassExp), parseExp(dynamicClassExp)]
+
+ delete attr.staticClassExp
+ delete attr.dynamicClassExp
+ } else {
+ const exps = getAttrExps(attr)
+ if (exps) {
+ attr.__exps = [exps]
+ }
+ }
+}
+
+function processStyle (attr) {
+ const { staticStyleExp = '', dynamicStyleExp = '' } = attr
+ if (staticStyleExp || dynamicStyleExp) {
+ attr.__exps = [parseExp(staticStyleExp), parseExp(dynamicStyleExp)]
+
+ delete attr.staticStyleExp
+ delete attr.dynamicStyleExp
+ } else {
+ const exps = getAttrExps(attr)
+ if (exps) {
+ attr.__exps = [exps]
+ }
+ }
+}
+
+function getAttrExps (attr) {
+ // 属性为单值的写法
+ // 默认置为 true
+ if (attr.value == null) {
+ attr.value = '{{ true }}'
+ }
+ const parsed = parseMustache(attr.value)
+ if (parsed.hasBinding && !attr.__exps) {
+ return parseExp(parsed.result)
+ }
+}
+
+function processBindEvent (attr) {
+ if (attr.eventConfigMap) {
+ const exps = []
+ for (const eventName in attr.eventConfigMap) {
+ const configs = attr.eventConfigMap[eventName] || []
+ const eventExp = {
+ eventName,
+ exps: []
+ }
+
+ configs.forEach((item) => {
+ eventExp.exps.push(parseExp(item))
+ })
+
+ exps.push(eventExp)
+ }
+
+ attr.__exps = exps
+
+ delete attr.eventConfigMap
+ }
+}
+
+function processText (vnode) {
+ // text 节点
+ if (vnode.type === 3) {
+ // todo 全局 defs 静态数的处理? -> 目前已经都支持了
+ const parsed = parseMustache(vnode.text)
+ if (parsed.hasBinding) {
+ vnode.__exps = parseExp(parsed.result)
+ delete vnode.text
+ }
+
+ delete vnode.exps
+ }
+}
+
+function processDirectives (vnode, config) {
+ const directives = Object.values(config.directive)
+ if (vnode.attrsMap) {
+ Object.keys(vnode.attrsMap).forEach(item => {
+ if (directives.includes(item)) {
+ getAndRemoveAttr(vnode, item)
+ }
+ })
+ }
+}
+
+function processChildren (vnode, config) {
+ if (vnode.children && vnode.children.length) {
+ vnode.children.forEach(item => {
+ simplifyTemplate(item, config)
+ })
+ } else {
+ delete vnode.children
+ }
+}
+
+function postProcessIf (vnode) {
+ // 删除遍历过程中 if 替换的临时节点以及明确不会被渲染出来的 if 节点(即 {{ false }})
+ const children = vnode.children
+ if (children && children.length) {
+ for (let i = children.length - 1; i >= 0; i--) {
+ if (children[i]._tempIf || children[i]._if === false) {
+ children.splice(i, 1)
+ }
+ }
+ }
+}
+
+function deleteUselessAttrs (vnode) {
+ const uselessAttrs = ['parent', 'exps', 'unary']
+ uselessAttrs.forEach(function (attr) {
+ delete vnode[attr]
+ })
+}
+
+function processWxs (vnode) {
+ if (vnode.tag === 'wxs') {
+ const tempNode = createASTElement('block', [])
+ replaceNode(vnode, tempNode)
+ return tempNode
+ }
+ return null
+}
+
+function simplifyTemplate (vnode, config) {
+ if (!vnode) {
+ return
+ }
+
+ const replacedBlockNode = processWxs(vnode)
+ if (replacedBlockNode) vnode = replacedBlockNode
+ processIf(vnode, config)
+ processFor(vnode)
+ processAttrsMap(vnode, config)
+ processText(vnode)
+ processChildren(vnode, config)
+ postProcessIf(vnode)
+
+ deleteUselessAttrs(vnode)
+
+ if (vnode.tag === 'temp-node') {
+ vnode.tag = 'block'
+ }
+}
+
+module.exports = function (vnode, mode) {
+ const _vnode = Object.assign({}, vnode)
+ const config = allConfigs[mode]
+ simplifyTemplate(_vnode, config)
+
+ return _vnode
+}
diff --git a/packages/webpack-plugin/lib/utils/check-is-runtime.js b/packages/webpack-plugin/lib/utils/check-is-runtime.js
new file mode 100644
index 0000000000..e2ec32928d
--- /dev/null
+++ b/packages/webpack-plugin/lib/utils/check-is-runtime.js
@@ -0,0 +1,7 @@
+const path = require('path')
+
+const RUNTIME_EXT_REG = /\.runtime(\.mpx)?/
+
+module.exports = function checkIsRuntimeMode (resource = '') {
+ return RUNTIME_EXT_REG.test(path.basename(resource))
+}
diff --git a/packages/webpack-plugin/lib/utils/const.js b/packages/webpack-plugin/lib/utils/const.js
index 5c02a6240f..2ff21d53f8 100644
--- a/packages/webpack-plugin/lib/utils/const.js
+++ b/packages/webpack-plugin/lib/utils/const.js
@@ -5,5 +5,11 @@ module.exports = {
RESOLVE_IGNORED_ERR: new Error('Resolve ignored!'),
JSON_JS_EXT: '.json.js',
MPX_ROOT_VIEW: 'mpx-root-view', // 根节点类名
- MPX_APP_MODULE_ID: 'mpx-app-scope' // app文件moduleId
+ MPX_APP_MODULE_ID: 'mpx-app-scope', // app文件moduleId
+ DYNAMIC: 'dynamic',
+ DYNAMIC_TEMPLATE: 'dynamic_template',
+ DYNAMIC_STYLE: 'dynamic_style',
+ BLOCK_TEMPLATE: 'template',
+ BLOCK_STYLES: 'styles',
+ BLOCK_JSON: 'json'
}
diff --git a/packages/webpack-plugin/lib/utils/resolve-mpx-custom-element-path.js b/packages/webpack-plugin/lib/utils/resolve-mpx-custom-element-path.js
new file mode 100644
index 0000000000..ec1622988b
--- /dev/null
+++ b/packages/webpack-plugin/lib/utils/resolve-mpx-custom-element-path.js
@@ -0,0 +1,7 @@
+module.exports = function (packageName) {
+ let subPath = ''
+ if (packageName !== 'main') {
+ subPath = '/' + packageName
+ }
+ return subPath + `/mpx-custom-element-${packageName}`
+}
diff --git a/packages/webpack-plugin/lib/wxml/loader.js b/packages/webpack-plugin/lib/wxml/loader.js
index 9318bf0708..962baa12c2 100644
--- a/packages/webpack-plugin/lib/wxml/loader.js
+++ b/packages/webpack-plugin/lib/wxml/loader.js
@@ -31,6 +31,10 @@ module.exports = function (content) {
const attributes = ['image:src', 'audio:src', 'video:src', 'cover-image:src', 'import:src', 'include:src', `${config[mode].wxs.tag}:${config[mode].wxs.src}`].concat(customAttributes)
+ // if (checkIsRuntimeMode(resourcePath)) {
+ // return 'module.exports = ' + JSON.stringify(content)
+ // }
+
const links = attrParse(content, function (tag, attr) {
const res = attributes.find(function (a) {
if (a.charAt(0) === ':') {