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) === ':') {