diff --git a/.eslintrc.js b/.eslintrc.js index 9b3f6a0218..71ffd9f02e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,6 +23,7 @@ module.exports = { App: 'readonly', __mpx_mode__: 'readonly', __mpx_env__: 'readonly', + __mpx_dynamic_runtime__: 'readonly', getRegExp: 'readonly', getCurrentPages: 'readonly' }, @@ -33,6 +34,6 @@ module.exports = { }, env: { 'jest/globals': true, - 'browser': true + browser: true } } diff --git a/docs-vuepress/articles/mpx2.md b/docs-vuepress/articles/mpx2.md index c533b31f11..9db576d5e1 100644 --- a/docs-vuepress/articles/mpx2.md +++ b/docs-vuepress/articles/mpx2.md @@ -776,7 +776,7 @@ class MPXProxy { this.miniRenderData = {} for (let key in renderData) { // 遍历数据访问路径 if (renderData.hasOwnProperty(key)) { - let item = renderData[key] + let item = renderData[key] let data = item[0] let firstKey = item[1] // 某个字段 path 的第一个 key 值 if (this.localKeys.indexOf(firstKey) > -1) { @@ -817,7 +817,7 @@ class MPXProxy { * @param {Object} renderData * @return {Object} processedRenderData */ -export function preprocessRenderData (renderData) { +export function preprocessRenderData (renderData) { // method for get key path array const processKeyPathMap = (keyPathMap) => { let keyPath = Object.keys(keyPathMap) diff --git a/packages/core/src/core/proxy.js b/packages/core/src/core/proxy.js index 76cc346c2f..7f6a407a32 100644 --- a/packages/core/src/core/proxy.js +++ b/packages/core/src/core/proxy.js @@ -44,6 +44,8 @@ import { ONHIDE, ONRESIZE } from './innerLifecycle' +import contextMap from '../dynamic/vnode/context' +import { getAst } from '../dynamic/astCache' let uid = 0 @@ -131,6 +133,8 @@ export default class MpxProxy { } created () { + // 缓存上下文,在 destoryed 阶段删除 + contextMap.set(this.uid, this.target) if (__mpx_mode__ !== 'web') { // web中BEFORECREATE钩子通过vue的beforeCreate钩子单独驱动 this.callHook(BEFORECREATE) @@ -190,6 +194,8 @@ export default class MpxProxy { } unmounted () { + // 页面/组件销毁清除上下文的缓存 + contextMap.remove(this.uid) this.callHook(BEFOREUNMOUNT) this.scope?.stop() if (this.update) this.update.active = false @@ -379,7 +385,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准备下次收集 @@ -478,6 +487,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) @@ -545,12 +573,30 @@ 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 __getAst = this.target.__getAst?.bind(this.target) + const moduleId = this.target.__moduleId + const dynamicTarget = this.target.__dynamic + const effect = this.effect = new ReactiveEffect(() => { // pre render for props update if (this.propsUpdatedFlag) { this.updatePreRender() } - + if (dynamicTarget || __getAst) { + try { + const ast = getAst(__getAst, moduleId) + return _r(false, _g(ast, moduleId)) + } catch (e) { + e.errType = 'mpx-dynamic-render' + e.errmsg = e.message + if (!__mpx_dynamic_runtime__) { + return error('Please make sure you have set dynamicRuntime true in mpx webpack plugin config because you have use the dynamic runtime feature.', this.options.mpxFileResource, e) + } else { + return error('Dynamic rendering error', this.options.mpxFileResource, e) + } + } + } if (this.target.__injectedRender) { try { return this.target.__injectedRender(_i, _c, _r, _sc) diff --git a/packages/core/src/dynamic/astCache.js b/packages/core/src/dynamic/astCache.js new file mode 100644 index 0000000000..9f4fb32cd3 --- /dev/null +++ b/packages/core/src/dynamic/astCache.js @@ -0,0 +1,25 @@ +import { isFunction, isObject, error } from '@mpxjs/utils' + +class DynamicAstCache { + #astCache = {} + + getAst (id) { + return this.#astCache[id] + } + + setAst (id, ast) { + this.#astCache[id] = ast + } +} + +export const dynamic = new DynamicAstCache() + +export const getAst = (__getAst, moduleId) => { + if ((__getAst && isFunction(__getAst))) { + const ast = __getAst() + if (!isObject(ast)) return error('__getAst returned data is not of type object') + return Object.values(ast)[0] + } else { + return dynamic.getAst(moduleId) + } +} diff --git a/packages/core/src/dynamic/dynamicRenderMixin.js b/packages/core/src/dynamic/dynamicRenderMixin.js new file mode 100644 index 0000000000..13eeba645d --- /dev/null +++ b/packages/core/src/dynamic/dynamicRenderMixin.js @@ -0,0 +1,77 @@ +import { hasOwn, isObject, error } from '@mpxjs/utils' +import genVnodeTree from './vnode/render' +import contextMap from './vnode/context' +import { CREATED } from '../core/innerLifecycle' + +function dynamicRefsMixin () { + return { + [CREATED] () { + // 处理ref场景,如果是在容器组件的上下文渲染 + if (this.mpxCustomElement) { + this._getRuntimeRefs() + } + }, + methods: { + _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 + } + }) + } + } + }) + } + } + } + } + } +} + +function dynamicSlotMixin () { + if (__mpx_mode__ === 'ali') { + return { + props: { slots: {} } + } + } else { + return { + properties: { slots: { type: Object } } + } + } +} + +function dynamicRenderHelperMixin () { + return { + methods: { + _g (astData, moduleId) { + const location = this.__mpxProxy && this.__mpxProxy.options.mpxFileResource + if (astData && isObject(astData) && hasOwn(astData, 'template')) { + const vnodeTree = genVnodeTree(astData, [this], { moduleId, location }) + return vnodeTree + } else { + error('Dynamic component get the wrong json ast data, please check.', location, { + errType: 'mpx-dynamic-render', + errmsg: 'invalid json ast data' + }) + } + } + } + } +} + +export { + dynamicRefsMixin, + dynamicSlotMixin, + dynamicRenderHelperMixin +} diff --git a/packages/core/src/dynamic/vnode/context.js b/packages/core/src/dynamic/vnode/context.js new file mode 100644 index 0000000000..0600c3e41e --- /dev/null +++ b/packages/core/src/dynamic/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/dynamic/vnode/css-select/cssauron.js b/packages/core/src/dynamic/vnode/css-select/cssauron.js new file mode 100644 index 0000000000..65a3aaa12f --- /dev/null +++ b/packages/core/src/dynamic/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/dynamic/vnode/css-select/index.js b/packages/core/src/dynamic/vnode/css-select/index.js new file mode 100644 index 0000000000..1429f43602 --- /dev/null +++ b/packages/core/src/dynamic/vnode/css-select/index.js @@ -0,0 +1,116 @@ +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) { + 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/dynamic/vnode/css-select/through.js b/packages/core/src/dynamic/vnode/css-select/through.js new file mode 100644 index 0000000000..654a349c06 --- /dev/null +++ b/packages/core/src/dynamic/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/dynamic/vnode/css-select/tokenizer.js b/packages/core/src/dynamic/vnode/css-select/tokenizer.js new file mode 100644 index 0000000000..ba0094c53b --- /dev/null +++ b/packages/core/src/dynamic/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/dynamic/vnode/interpreter.js b/packages/core/src/dynamic/vnode/interpreter.js new file mode 100644 index 0000000000..9775d000ad --- /dev/null +++ b/packages/core/src/dynamic/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/dynamic/vnode/render.js b/packages/core/src/dynamic/vnode/render.js new file mode 100644 index 0000000000..20e3d5cca9 --- /dev/null +++ b/packages/core/src/dynamic/vnode/render.js @@ -0,0 +1,289 @@ +import cssSelect from './css-select' +// todo: stringify wxs 模块只能放到逻辑层执行,主要还是因为生成 vdom tree 需要根据 class 去做匹配,需要看下这个代码从哪引入 +import stringify from '@mpxjs/webpack-plugin/lib/runtime/stringify.wxs' +import Interpreter from './interpreter' +import { dash2hump, isString, error } 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 +} + +export default function _genVnodeTree (astData, contextScope, options) { + const { template = {}, styles = [] } = astData || {} + const { moduleId, location } = options || {} + // 解除引用 + const templateAst = deepCloneNode(template) + // 获取实例 uid + const uid = contextScope[0]?.__mpxProxy?.uid || contextScope[0]?.uid + // 动态化组件 slots 通过上下文传递,相当于 props + 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.map(item => genVnodeWithStaticCss(deepCloneNode(item)))) + children = [] + } + return createNode(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) { + const errmsg = e.message + console.warn(errmsg) + error('interprete the expression wrong: ', location, { + errType: 'mpx-dynamic-interprete', + errmsg + }) + } + 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.d?.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.__exp) { + let valueArr = evalExps(attr.__exp) + valueArr = Array.isArray(valueArr) ? valueArr : [valueArr] + value = helper(...valueArr) + // dynamic style + wx:show + const showStyle = valueArr[2] + if (showStyle) { + value = value + ';' + showStyle + } + } else { + value = attr.value + } + res[attr.name] = value + } else { + res[dash2hump(attr.name)] = attr.__exp + ? evalExps(attr.__exp) + : 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.__exp ? evalExps(node.__exp) : 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.__exp) + + // 和微信的模版渲染策略保持一致:当 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.__exp) + 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) { + styles.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(templateAst) + + return genVnodeWithStaticCss(interpreteredVnodeTree) +} diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 908ae6d7eb..efac8c8992 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -60,6 +60,8 @@ export { export { getMixin } from './core/mergeOptions' +export { dynamic } from './dynamic/astCache' + export function toPureObject (obj) { return diffAndCloneA(obj).clone } diff --git a/packages/core/src/platform/builtInMixins/index.js b/packages/core/src/platform/builtInMixins/index.js index c7c54e63f5..d9d903c32e 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 { dynamicRefsMixin, dynamicRenderHelperMixin, dynamicSlotMixin } from '../../dynamic/dynamicRenderMixin' export default function getBuiltInMixins (options, type) { let bulitInMixins = [] @@ -41,6 +42,13 @@ export default function getBuiltInMixins (options, type) { showMixin(type), i18nMixin() ]) + if (__mpx_dynamic_runtime__) { + bulitInMixins = bulitInMixins.concat([ + dynamicRenderHelperMixin(), + dynamicSlotMixin(), + dynamicRefsMixin() + ]) + } } } return bulitInMixins.filter(item => item) diff --git a/packages/core/src/platform/builtInMixins/proxyEventMixin.js b/packages/core/src/platform/builtInMixins/proxyEventMixin.js index f69ee196f3..948d6116b0 100644 --- a/packages/core/src/platform/builtInMixins/proxyEventMixin.js +++ b/packages/core/src/platform/builtInMixins/proxyEventMixin.js @@ -1,5 +1,11 @@ import { setByPath, error, dash2hump, collectDataset } from '@mpxjs/utils' import Mpx from '../../index' +import contextMap from '../../dynamic/vnode/context' + +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 = { @@ -33,6 +39,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] @@ -56,10 +65,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/renderHelperMixin.js b/packages/core/src/platform/builtInMixins/renderHelperMixin.js index c90baefa44..dc8859c2b4 100644 --- a/packages/core/src/platform/builtInMixins/renderHelperMixin.js +++ b/packages/core/src/platform/builtInMixins/renderHelperMixin.js @@ -36,8 +36,8 @@ 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) } } } diff --git a/packages/core/src/platform/patch/ali/getDefaultOptions.js b/packages/core/src/platform/patch/ali/getDefaultOptions.js index 70ac339880..5cc74e7678 100644 --- a/packages/core/src/platform/patch/ali/getDefaultOptions.js +++ b/packages/core/src/platform/patch/ali/getDefaultOptions.js @@ -63,6 +63,26 @@ function transformApiForProxy (context, currentInject) { } }) } + if (currentInject.moduleId) { + Object.defineProperties(context, { + __moduleId: { + get () { + return currentInject.moduleId + }, + configurable: false + } + }) + } + if (currentInject.dynamic) { + Object.defineProperties(context, { + __dynamic: { + get () { + return currentInject.dynamic + }, + configurable: false + } + }) + } } } diff --git a/packages/core/src/platform/patch/wx/getDefaultOptions.js b/packages/core/src/platform/patch/wx/getDefaultOptions.js index b3fc09f035..33c37f6a29 100644 --- a/packages/core/src/platform/patch/wx/getDefaultOptions.js +++ b/packages/core/src/platform/patch/wx/getDefaultOptions.js @@ -101,6 +101,26 @@ function transformApiForProxy (context, currentInject) { } }) } + if (currentInject.moduleId) { + Object.defineProperties(context, { + __moduleId: { + get () { + return currentInject.moduleId + }, + configurable: false + } + }) + } + if (currentInject.dynamic) { + Object.defineProperties(context, { + __dynamic: { + get () { + return currentInject.dynamic + }, + configurable: false + } + }) + } } } diff --git a/packages/webpack-plugin/lib/dependencies/DynamicEntryDependency.js b/packages/webpack-plugin/lib/dependencies/DynamicEntryDependency.js index dba03d2463..608defcacf 100644 --- a/packages/webpack-plugin/lib/dependencies/DynamicEntryDependency.js +++ b/packages/webpack-plugin/lib/dependencies/DynamicEntryDependency.js @@ -5,6 +5,7 @@ const addQuery = require('../utils/add-query') const toPosix = require('../utils/to-posix') const async = require('async') const parseRequest = require('../utils/parse-request') +const hasOwn = require('../utils/has-own') class DynamicEntryDependency extends NullDependency { constructor (range, request, entryType, outputPath = '', packageRoot = '', relativePath = '', context = '', extraOptions = {}) { @@ -131,6 +132,13 @@ class DynamicEntryDependency extends NullDependency { this.publicPath = compilation.outputOptions.publicPath || '' const { packageRoot, context } = this if (context) this.resolver = compilation.resolverFactory.get('normal', module.resolveOptions) + // post 分包队列在 sub 分包队列构建完毕后进行 + if (this.extraOptions.postSubpackageEntry) { + mpx.postSubpackageEntriesMap[packageRoot] = mpx.postSubpackageEntriesMap[packageRoot] || [] + mpx.postSubpackageEntriesMap[packageRoot].push(this) + callback() + return + } // 分包构建在需要在主包构建完成后在finishMake中处理,返回的资源路径先用key来占位,在合成extractedAssets时再进行最终替换 if (packageRoot && mpx.currentPackageRoot !== packageRoot) { mpx.subpackagesEntriesMap[packageRoot] = mpx.subpackagesEntriesMap[packageRoot] || [] @@ -190,7 +198,7 @@ DynamicEntryDependency.Template = class DynamicEntryDependencyTemplate { let replaceContent = '' - if (extraOptions.replaceContent) { + if (hasOwn(extraOptions, 'replaceContent')) { replaceContent = extraOptions.replaceContent } else if (resultPath) { if (extraOptions.isRequireAsync) { diff --git a/packages/webpack-plugin/lib/dependencies/RecordRuntimeInfoDependency.js b/packages/webpack-plugin/lib/dependencies/RecordRuntimeInfoDependency.js new file mode 100644 index 0000000000..db737b71c9 --- /dev/null +++ b/packages/webpack-plugin/lib/dependencies/RecordRuntimeInfoDependency.js @@ -0,0 +1,66 @@ +const NullDependency = require('webpack/lib/dependencies/NullDependency') +const makeSerializable = require('webpack/lib/util/makeSerializable') + +class RecordRuntimeInfoDependency extends NullDependency { + constructor (packageName, resourcePath, { type, info, index } = {}) { + super() + this.packageName = packageName + this.resourcePath = resourcePath + this.blockType = type + this.info = info + this.index = index + } + + get type () { + return 'mpx record runtime component info' + } + + mpxAction (module, compilation, callback) { + const mpx = compilation.__mpx__ + + const runtimeInfoPackage = mpx.runtimeInfo[this.packageName] = mpx.runtimeInfo[this.packageName] || {} + const componentInfo = runtimeInfoPackage[this.resourcePath] = runtimeInfoPackage[this.resourcePath] || { + template: {}, + json: {}, + style: [], + moduleId: '_' + mpx.pathHash(this.resourcePath) + } + + const infoConfig = componentInfo[this.blockType] + if (this.blockType === 'style') { // 多 style block 的场景 + infoConfig[this.index] = this.info + } else { + Object.assign(infoConfig, this.info) + } + + return callback() + } + + serialize (context) { + const { write } = context + write(this.packageName) + write(this.resourcePath) + write(this.blockType) + write(this.info) + write(this.index) + super.serialize(context) + } + + deserialize (context) { + const { read } = context + this.packageName = read() + this.resourcePath = read() + this.blockType = read() + this.info = read() + this.index = read() + super.deserialize(context) + } +} + +RecordRuntimeInfoDependency.Template = class RecordRuntimeInfoDependencyTemplate { + apply () {} +} + +makeSerializable(RecordRuntimeInfoDependency, '@mpxjs/webpack-plugin/lib/dependencies/RecordRuntimeInfoDependency') + +module.exports = RecordRuntimeInfoDependency diff --git a/packages/webpack-plugin/lib/index.js b/packages/webpack-plugin/lib/index.js index 8747a9de17..3f54df5cc5 100644 --- a/packages/webpack-plugin/lib/index.js +++ b/packages/webpack-plugin/lib/index.js @@ -40,6 +40,7 @@ const DynamicEntryDependency = require('./dependencies/DynamicEntryDependency') const FlagPluginDependency = require('./dependencies/FlagPluginDependency') const RemoveEntryDependency = require('./dependencies/RemoveEntryDependency') const RecordVueContentDependency = require('./dependencies/RecordVueContentDependency') +const RecordRuntimeInfoDependency = require('./dependencies/RecordRuntimeInfoDependency') const SplitChunksPlugin = require('webpack/lib/optimize/SplitChunksPlugin') const fixRelative = require('./utils/fix-relative') const parseRequest = require('./utils/parse-request') @@ -63,6 +64,7 @@ const stringifyLoadersAndResource = require('./utils/stringify-loaders-resource' const emitFile = require('./utils/emit-file') const { MPX_PROCESSED_FLAG, MPX_DISABLE_EXTRACTOR_CACHE } = require('./utils/const') const isEmptyObject = require('./utils/is-empty-object') +const DynamicPlugin = require('./resolver/DynamicPlugin') require('./utils/check-core-version-match') const isProductionLikeMode = options => { @@ -123,6 +125,9 @@ class MpxWebpackPlugin { if (options.mode === 'web' && options.srcMode !== 'wx') { errors.push('MpxWebpackPlugin supports mode to be "web" only when srcMode is set to "wx"!') } + if (options.dynamicComponentRules && !options.dynamicRuntime) { + errors.push('Please make sure you have set dynamicRuntime true in mpx webpack plugin config because you have use the dynamic runtime feature.') + } options.externalClasses = options.externalClasses || ['custom-class', 'i-class'] options.resolveMode = options.resolveMode || 'webpack' options.writeMode = options.writeMode || 'changed' @@ -136,7 +141,8 @@ class MpxWebpackPlugin { options.defs = Object.assign({}, options.defs, { __mpx_mode__: options.mode, __mpx_src_mode__: options.srcMode, - __mpx_env__: options.env + __mpx_env__: options.env, + __mpx_dynamic_runtime__: options.dynamicRuntime }) // 批量指定源码mode options.modeRules = options.modeRules || {} @@ -174,6 +180,7 @@ class MpxWebpackPlugin { options.optimizeRenderRules = options.optimizeRenderRules ? (Array.isArray(options.optimizeRenderRules) ? options.optimizeRenderRules : [options.optimizeRenderRules]) : [] options.retryRequireAsync = options.retryRequireAsync || false options.optimizeSize = options.optimizeSize || false + options.dynamicComponentRules = options.dynamicComponentRules || {}// 运行时组件配置 this.options = options // Hack for buildDependencies const rawResolveBuildDependencies = FileSystemInfo.prototype.resolveBuildDependencies @@ -320,6 +327,9 @@ class MpxWebpackPlugin { 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') + + const dynamicPlugin = new DynamicPlugin('result', this.options.dynamicComponentRules) + if (Array.isArray(compiler.options.resolve.plugins)) { compiler.options.resolve.plugins.push(addModePlugin) } else { @@ -330,6 +340,7 @@ class MpxWebpackPlugin { } compiler.options.resolve.plugins.push(packageEntryPlugin) compiler.options.resolve.plugins.push(new FixDescriptionInfoPlugin()) + compiler.options.resolve.plugins.push(dynamicPlugin) const optimization = compiler.options.optimization if (this.options.mode !== 'web') { @@ -472,12 +483,12 @@ class MpxWebpackPlugin { } } - const processSubpackagesEntriesMap = (compilation, callback) => { + const processSubpackagesEntriesMap = (subPackageEntriesType, compilation, callback) => { const mpx = compilation.__mpx__ - if (mpx && !isEmptyObject(mpx.subpackagesEntriesMap)) { - const subpackagesEntriesMap = mpx.subpackagesEntriesMap - // 执行分包队列前清空mpx.subpackagesEntriesMap - mpx.subpackagesEntriesMap = {} + if (mpx && !isEmptyObject(mpx[subPackageEntriesType])) { + const subpackagesEntriesMap = mpx[subPackageEntriesType] + // 执行分包队列前清空 mpx[subPackageEntriesType] + mpx[subPackageEntriesType] = {} async.eachOfSeries(subpackagesEntriesMap, (deps, packageRoot, callback) => { mpx.currentPackageRoot = packageRoot mpx.componentsMap[packageRoot] = mpx.componentsMap[packageRoot] || {} @@ -493,7 +504,7 @@ class MpxWebpackPlugin { }, (err) => { if (err) return callback(err) // 如果执行完当前队列后产生了新的分包执行队列(一般由异步分包组件造成),则继续执行 - processSubpackagesEntriesMap(compilation, callback) + processSubpackagesEntriesMap(subPackageEntriesType, compilation, callback) }) } else { callback() @@ -505,7 +516,14 @@ class MpxWebpackPlugin { name: 'MpxWebpackPlugin', stage: -1000 }, (compilation, callback) => { - processSubpackagesEntriesMap(compilation, (err) => { + async.series([ + (callback) => { + processSubpackagesEntriesMap('subpackagesEntriesMap', compilation, callback) + }, + (callback) => { + processSubpackagesEntriesMap('postSubpackageEntriesMap', compilation, callback) + } + ], (err) => { if (err) return callback(err) const checkDynamicEntryInfo = () => { for (const packageName in mpx.dynamicEntryInfo) { @@ -578,6 +596,9 @@ class MpxWebpackPlugin { compilation.dependencyFactories.set(RecordVueContentDependency, new NullFactory()) compilation.dependencyTemplates.set(RecordVueContentDependency, new RecordVueContentDependency.Template()) + compilation.dependencyFactories.set(RecordRuntimeInfoDependency, new NullFactory()) + compilation.dependencyTemplates.set(RecordRuntimeInfoDependency, new RecordRuntimeInfoDependency.Template()) + compilation.dependencyTemplates.set(ImportDependency, new ImportDependencyTemplate()) }) @@ -610,6 +631,7 @@ class MpxWebpackPlugin { // 记录独立分包 independentSubpackagesMap: {}, subpackagesEntriesMap: {}, + postSubpackageEntriesMap: {}, replacePathMap: {}, exportModules: new Set(), // 记录动态添加入口的分包信息 @@ -837,6 +859,126 @@ class MpxWebpackPlugin { error }) } + }, + // 以包为维度记录不同 package 需要的组件属性等信息,用以最终 mpx-custom-element 相关文件的输出 + runtimeInfo: {}, + // 记录运行时组件依赖的运行时组件当中使用的基础组件 slot,最终依据依赖关系注入到运行时组件的 json 配置当中 + dynamicSlotDependencies: {}, + // 依据 package 注入到 mpx-custom-element-*.json 里面的组件路径 + getPackageInjectedComponentsMap: (packageName = 'main') => { + const res = {} + const runtimeInfo = mpx.runtimeInfo[packageName] || {} + const componentsMap = mpx.componentsMap[packageName] || {} + const publicPath = compilation.outputOptions.publicPath || '' + for (const componentPath in runtimeInfo) { + Object.values(runtimeInfo[componentPath].json).forEach(({ hashName, resourcePath }) => { + const outputPath = componentsMap[resourcePath] + if (outputPath) { + res[hashName] = publicPath + outputPath + } + }) + } + return res + }, + // 获取生成基础递归渲染模版的节点配置信息 + getPackageInjectedTemplateConfig: (packageName = 'main') => { + const res = { + baseComponents: { + block: {} + }, + runtimeComponents: {}, + normalComponents: {} + } + + const runtimeInfo = mpx.runtimeInfo[packageName] || {} + + // 包含了某个分包当中所有的运行时组件 + for (const resourcePath in runtimeInfo) { + const { json, template } = runtimeInfo[resourcePath] + const customComponents = template.customComponents || {} + const baseComponents = template.baseComponents || {} + + // 合并自定义组件的属性 + for (const componentName in customComponents) { + const extraAttrs = {} + const attrsMap = customComponents[componentName] + const { hashName, isDynamic } = json[componentName] || {} + let componentType = 'normalComponents' + if (isDynamic) { + componentType = 'runtimeComponents' + extraAttrs.slots = '' + } + if (!res[componentType][hashName]) { + res[componentType][hashName] = {} + } + + Object.assign(res[componentType][hashName], { + ...attrsMap, + ...extraAttrs + }) + } + + // 合并基础节点的属性 + for (const componentName in baseComponents) { + const attrsMap = baseComponents[componentName] + if (!res.baseComponents[componentName]) { + res.baseComponents[componentName] = {} + } + Object.assign(res.baseComponents[componentName], attrsMap) + } + } + + return res + }, + injectDynamicSlotDependencies: (usingComponents, resourcePath) => { + const dynamicSlotReg = /"mpx_dynamic_slot":\s*""*/ + const content = mpx.dynamicSlotDependencies[resourcePath] ? JSON.stringify(mpx.dynamicSlotDependencies[resourcePath]).slice(1, -1) : '' + const result = usingComponents.replace(dynamicSlotReg, content).replace(/,\s*(\}|\])/g, '$1') + return result + }, + changeHashNameForAstNode: (templateAst, componentsMap) => { + const nameReg = /"tag":\s*"([^"]*)",?/g + + // 替换 astNode hashName + const result = templateAst.replace(nameReg, function (match, tag) { + if (componentsMap[tag]) { + const { hashName, isDynamic } = componentsMap[tag] + let content = `"tag": "${hashName}",` + if (isDynamic) { + content += '"dynamic": true,' + } + return content + } + return match + }).replace(/,\s*(\}|\])/g, '$1') + + return result + }, + collectDynamicSlotDependencies: (packageName = 'main') => { + const componentsMap = mpx.componentsMap[packageName] || {} + const publicPath = compilation.outputOptions.publicPath || '' + const runtimeInfo = mpx.runtimeInfo[packageName] + + for (const resourcePath in runtimeInfo) { + const { template, json } = runtimeInfo[resourcePath] + const dynamicSlotDependencies = template.dynamicSlotDependencies || [] + + dynamicSlotDependencies.forEach((slotDependencies) => { + let lastNeedInjectNode = slotDependencies[0] + for (let i = 1; i <= slotDependencies.length - 1; i++) { + const componentName = slotDependencies[i] + const { resourcePath, isDynamic } = json[componentName] || {} + if (isDynamic) { + const { resourcePath: path, hashName } = json[lastNeedInjectNode] + mpx.dynamicSlotDependencies[resourcePath] = mpx.dynamicSlotDependencies[resourcePath] || {} + Object.assign(mpx.dynamicSlotDependencies[resourcePath], { + [hashName]: publicPath + componentsMap[path] + }) + lastNeedInjectNode = slotDependencies[i] + } + } + }) + } } } } @@ -1094,6 +1236,37 @@ class MpxWebpackPlugin { } }) + compilation.hooks.processAssets.tap({ + name: 'MpxWebpackPlugin' + }, (assets) => { + const dynamicAssets = {} + for (const packageName in mpx.runtimeInfo) { + for (const resourcePath in mpx.runtimeInfo[packageName]) { + const { moduleId, template, style, json } = mpx.runtimeInfo[packageName][resourcePath] + const templateAst = mpx.changeHashNameForAstNode(template.templateAst, json) + dynamicAssets[moduleId] = { + template: JSON.parse(templateAst), + styles: style.reduce((preV, curV) => { + preV.push(...curV) + return preV + }, []) + } + + // 注入 dynamic slot dependency + const outputPath = mpx.componentsMap[packageName][resourcePath] + if (outputPath) { + const jsonAsset = outputPath + '.json' + const jsonContent = compilation.assets[jsonAsset].source() + compilation.assets[jsonAsset] = new RawSource(mpx.injectDynamicSlotDependencies(jsonContent, resourcePath)) + } + } + } + if (!isEmptyObject(dynamicAssets)) { + // 产出 jsonAst 静态产物 + compilation.assets['dynamic.json'] = new RawSource(JSON.stringify(dynamicAssets)) + } + }) + const normalModuleFactoryParserCallback = (parser) => { parser.hooks.call.for('__mpx_resolve_path__').tap('MpxWebpackPlugin', (expr) => { if (expr.arguments[0]) { diff --git a/packages/webpack-plugin/lib/json-compiler/helper.js b/packages/webpack-plugin/lib/json-compiler/helper.js index 4417f19aef..09a8622b58 100644 --- a/packages/webpack-plugin/lib/json-compiler/helper.js +++ b/packages/webpack-plugin/lib/json-compiler/helper.js @@ -99,7 +99,9 @@ module.exports = function createJSONHelper ({ loaderContext, emitWarning, custom const entry = getDynamicEntry(resource, 'component', outputPath, tarRoot, relativePath, '', extraOptions) callback(null, entry, { tarRoot, - placeholder + placeholder, + resourcePath, + queryObj }) }) } diff --git a/packages/webpack-plugin/lib/json-compiler/index.js b/packages/webpack-plugin/lib/json-compiler/index.js index 243e6bf47a..444b47813d 100644 --- a/packages/webpack-plugin/lib/json-compiler/index.js +++ b/packages/webpack-plugin/lib/json-compiler/index.js @@ -12,12 +12,14 @@ const createHelpers = require('../helpers') const createJSONHelper = require('./helper') const RecordGlobalComponentsDependency = require('../dependencies/RecordGlobalComponentsDependency') const RecordIndependentDependency = require('../dependencies/RecordIndependentDependency') +const RecordRuntimeInfoDependency = require('../dependencies/RecordRuntimeInfoDependency') const { MPX_DISABLE_EXTRACTOR_CACHE, RESOLVE_IGNORED_ERR, JSON_JS_EXT } = require('../utils/const') const resolve = require('../utils/resolve') const resolveTabBarPath = require('../utils/resolve-tab-bar-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') +const resolveMpxCustomElementPath = require('../utils/resolve-mpx-custom-element-path') module.exports = function (content) { const nativeCallback = this.async() @@ -47,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 = queryObj.isDynamic const emitWarning = (msg) => { this.emitWarning( @@ -173,6 +176,17 @@ module.exports = function (content) { } } + const dependencyComponentsMap = {} + + if (queryObj.mpxCustomElement) { + this.cacheable(false) + mpx.collectDynamicSlotDependencies(packageName) + } + + if (runtimeCompile) { + json.usingComponents = json.usingComponents || {} + } + // 快应用补全json配置,必填项 if (mode === 'qa' && isApp) { const defaultConf = { @@ -219,13 +233,24 @@ module.exports = function (content) { 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, queryObj = {} } = {}) => { if (err === RESOLVE_IGNORED_ERR) { delete components[name] return callback() } if (err) return callback(err) components[name] = entry + if (runtimeCompile) { + // 替换组件的 hashName,并删除原有的组件配置 + const hashName = 'm' + mpx.pathHash(resourcePath) + components[hashName] = entry + delete components[name] + dependencyComponentsMap[name] = { + hashName, + resourcePath, + isDynamic: queryObj.isDynamic + } + } if (tarRoot) { if (placeholder) { placeholder = normalizePlaceholder(placeholder) @@ -250,7 +275,20 @@ module.exports = function (content) { callback() } }) - }, callback) + }, () => { + const mpxCustomElementPath = resolveMpxCustomElementPath(packageName) + if (runtimeCompile) { + components.element = mpxCustomElementPath + components.mpx_dynamic_slot = '' // 运行时组件打标记,在 processAssets 统一替换 + + this._module.addPresentationalDependency(new RecordRuntimeInfoDependency(packageName, resourcePath, { type: 'json', info: dependencyComponentsMap })) + } + if (queryObj.mpxCustomElement) { + components.element = mpxCustomElementPath + Object.assign(components, mpx.getPackageInjectedComponentsMap(packageName)) + } + callback() + }) } else { callback() } diff --git a/packages/webpack-plugin/lib/loader.js b/packages/webpack-plugin/lib/loader.js index 4a7c5b4c3f..8fe76ba1c7 100644 --- a/packages/webpack-plugin/lib/loader.js +++ b/packages/webpack-plugin/lib/loader.js @@ -16,11 +16,13 @@ const AppEntryDependency = require('./dependencies/AppEntryDependency') const RecordResourceMapDependency = require('./dependencies/RecordResourceMapDependency') const RecordVueContentDependency = require('./dependencies/RecordVueContentDependency') const CommonJsVariableDependency = require('./dependencies/CommonJsVariableDependency') +const DynamicEntryDependency = require('./dependencies/DynamicEntryDependency') const tsWatchRunLoaderFilter = require('./utils/ts-loader-watch-run-loader-filter') const { MPX_APP_MODULE_ID } = require('./utils/const') const path = require('path') const processMainScript = require('./web/processMainScript') const getRulesRunner = require('./platform') +const genMpxCustomElement = require('./runtime-render/gen-mpx-custom-element') module.exports = function (content) { this.cacheable() @@ -50,6 +52,7 @@ module.exports = function (content) { const localSrcMode = queryObj.mode const srcMode = localSrcMode || globalSrcMode const autoScope = matchCondition(resourcePath, mpx.autoScopeRules) + const isRuntimeMode = queryObj.isDynamic const emitWarning = (msg) => { this.emitWarning( @@ -80,6 +83,12 @@ module.exports = function (content) { const appName = getEntryName(this) if (appName) this._module.addPresentationalDependency(new AppEntryDependency(resourcePath, appName)) } + + if (isRuntimeMode) { + const { request, outputPath } = genMpxCustomElement(packageName) + this._module.addPresentationalDependency(new DynamicEntryDependency([0, 0], request, 'component', outputPath, packageRoot, '', '', { replaceContent: '', postSubpackageEntry: true })) + } + const loaderContext = this const isProduction = this.minimize || process.env.NODE_ENV === 'production' const filePath = this.resourcePath diff --git a/packages/webpack-plugin/lib/resolver/DynamicPlugin.js b/packages/webpack-plugin/lib/resolver/DynamicPlugin.js new file mode 100644 index 0000000000..977b1cb426 --- /dev/null +++ b/packages/webpack-plugin/lib/resolver/DynamicPlugin.js @@ -0,0 +1,18 @@ +const addQuery = require('../utils/add-query') +const { matchCondition } = require('../utils/match-condition') + +module.exports = class DynamicPlugin { + constructor (source, dynamicComponentRules) { + this.source = source + this.dynamicComponentRules = dynamicComponentRules + } + + apply (resolver) { + resolver.getHook(this.source).tap('DynamicPlugin', request => { + const isDynamic = matchCondition(request.path, this.dynamicComponentRules) + if (isDynamic) { + request.query = addQuery(request.query, { isDynamic: true }) + } + }) + } +} 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..890884ec56 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime-render/base-wxml.js @@ -0,0 +1,75 @@ +const { getOptimizedComponentInfo } = require('@mpxjs/template-engine/dist/optimizer') +const mpxConfig = require('../config') + +function makeAttrsMap (attrKeys = []) { + return attrKeys.reduce((preVal, curVal) => Object.assign(preVal, { [curVal]: '' }), {}) +} + +// 部分节点类型不需要被收集 +const RUNTIME_FILTER_NODES = ['import', 'template', 'wxs', 'component', 'slot'] + +function collectParentCustomComponent (el, isComponentNode, options) { + const res = [] + let parent = el.parent + while (parent) { + if (isComponentNode(parent, options)) { + if (!res.length) res.push(el.tag) + res.push(parent.tag) + } + parent = parent.parent + } + return res +} + +module.exports = function setBaseWxml (el, config, meta) { + const { mode, isComponentNode, options } = config + if (RUNTIME_FILTER_NODES.includes(el.tag)) { + return + } + + if (options.runtimeCompile) { + const isCustomComponent = isComponentNode(el, options) + + if (!meta.runtimeInfo) { + meta.runtimeInfo = { + baseComponents: {}, + customComponents: {}, + dynamicSlotDependencies: [] + } + } + + const tag = el.tag + // 属性收集 + const modeConfig = mpxConfig[mode] + const directives = new Set([...Object.values(modeConfig.directive), 'slot']) + const attrKeys = Object.keys(el.attrsMap).filter(key => !directives.has(key)) + const componentType = isCustomComponent ? 'customComponents' : 'baseComponents' + + if (!isCustomComponent) { + const optimizedInfo = getOptimizedComponentInfo( + { + nodeType: el.tag, + attrs: el.attrsMap + }, + mode + ) + if (optimizedInfo) { + el.tag = optimizedInfo.nodeType + } + } else { + // 收集运行时组件模版当中运行时组件使用 slot 的场景,主要因为运行时组件渲染slot时组件上下文发生了变化 + const slotDependencies = collectParentCustomComponent(el, isComponentNode, options) + if (slotDependencies.length) { + const dynamicSlotDependencies = meta.runtimeInfo.dynamicSlotDependencies + dynamicSlotDependencies.push(slotDependencies) + } + } + + const componentsConfig = meta.runtimeInfo[componentType] + + if (!componentsConfig[tag]) { + componentsConfig[tag] = {} + } + Object.assign(componentsConfig[tag], makeAttrsMap(attrKeys)) + } +} diff --git a/packages/webpack-plugin/lib/runtime-render/custom-element-json.js b/packages/webpack-plugin/lib/runtime-render/custom-element-json.js new file mode 100644 index 0000000000..5cd736d6da --- /dev/null +++ b/packages/webpack-plugin/lib/runtime-render/custom-element-json.js @@ -0,0 +1,4 @@ +module.exports = { + component: true, + usingComponents: {} +} diff --git a/packages/webpack-plugin/lib/runtime-render/custom-element-script.js b/packages/webpack-plugin/lib/runtime-render/custom-element-script.js new file mode 100644 index 0000000000..261f791d0f --- /dev/null +++ b/packages/webpack-plugin/lib/runtime-render/custom-element-script.js @@ -0,0 +1,32 @@ +import { createComponent } from '@mpxjs/core' + +createComponent({ + options: { + addGlobalClass: true, + styleIsolation: 'shared', + // 超过基础模板的层数,virtualHost 置为 true,避免样式规则失效 + virtualHost: true + }, + properties: { + r: { // vdom 数据 + type: Object, + value: { + nt: 'block' + } + } + }, + data: { + // 运行时组件的标识 + mpxCustomElement: true + }, + computed: { + vnodeData () { + const data = this.r.d || {} + return data + }, + // 真实的组件上下文 uid + uid () { + return this.vnodeData.uid + } + } +}) diff --git a/packages/webpack-plugin/lib/runtime-render/gen-dynamic-template.js b/packages/webpack-plugin/lib/runtime-render/gen-dynamic-template.js new file mode 100644 index 0000000000..9e223bb9b6 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime-render/gen-dynamic-template.js @@ -0,0 +1,6 @@ +const { createSetupTemplate } = require('@mpxjs/template-engine') + +module.exports = function (packageName) { + const basePath = packageName === 'main' ? '' : `/${packageName}` + return `${createSetupTemplate()}` +} diff --git a/packages/webpack-plugin/lib/runtime-render/gen-mpx-custom-element.js b/packages/webpack-plugin/lib/runtime-render/gen-mpx-custom-element.js new file mode 100644 index 0000000000..fe7f815bb7 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime-render/gen-mpx-custom-element.js @@ -0,0 +1,13 @@ +const path = require('path') + +module.exports = function (packageName) { + const baseName = 'mpx-custom-element' + const rawFile = baseName + (packageName === 'main' ? '-main' : '') + const filePath = path.resolve(__dirname, `${rawFile}.mpx`) + const request = `${filePath}?mpxCustomElement&isComponent&p=${packageName}` + + return { + request, + outputPath: baseName + '-' + packageName + } +} diff --git a/packages/webpack-plugin/lib/runtime-render/mpx-custom-element-main.mpx b/packages/webpack-plugin/lib/runtime-render/mpx-custom-element-main.mpx new file mode 100644 index 0000000000..574a711845 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime-render/mpx-custom-element-main.mpx @@ -0,0 +1,18 @@ + + + + + + + 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..574a711845 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime-render/mpx-custom-element.mpx @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/webpack-plugin/lib/style-compiler/index.js b/packages/webpack-plugin/lib/style-compiler/index.js index f7400ae0e7..820b1a9c48 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 } = require('../utils/const') +const { MPX_ROOT_VIEW, MPX_DISABLE_EXTRACTOR_CACHE } = 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 RecordRuntimeInfoDependency = require('../dependencies/RecordRuntimeInfoDependency') module.exports = function (css, map) { this.cacheable() @@ -22,6 +24,9 @@ module.exports = function (css, map) { const isApp = resourcePath === appInfo.resourcePath const transRpxRulesRaw = mpx.transRpxRules const transRpxRules = transRpxRulesRaw ? (Array.isArray(transRpxRulesRaw) ? transRpxRulesRaw : [transRpxRulesRaw]) : [] + const runtimeCompile = queryObj.isDynamic + const index = queryObj.index || 0 + const packageName = queryObj.packageRoot || mpx.currentPackageRoot || 'main' const transRpxFn = mpx.webConfig.transRpxFn const testResolveRange = (include = () => true, exclude) => { @@ -85,6 +90,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 +138,13 @@ module.exports = function (css, map) { } } + if (runtimeCompile) { + // 包含了运行时组件的 style 模块必须每次都创建(但并不是每次都需要build),用于收集组件节点信息,传递信息以禁用父级extractor的缓存 + this.emitFile(MPX_DISABLE_EXTRACTOR_CACHE, '', undefined, { skipEmit: true }) + this._module.addPresentationalDependency(new RecordRuntimeInfoDependency(packageName, resourcePath, { type: 'style', info: cssList, index })) + 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 1bc2483a93..2a0020b3fe 100644 --- a/packages/webpack-plugin/lib/template-compiler/compiler.js +++ b/packages/webpack-plugin/lib/template-compiler/compiler.js @@ -12,6 +12,9 @@ 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 { parseExp } = require('./parse-exps') + const shallowStringify = require('../utils/shallow-stringify') const no = function () { return false @@ -677,6 +680,7 @@ function parse (template, options) { const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag) const element = createASTElement(tag, attrs, currentParent) + if (ns) { element.ns = ns } @@ -743,7 +747,7 @@ function parse (template, options) { parent: currentParent } children.push(el) - processText(el, meta, options) + options.runtimeCompile ? processTextDynamic(el) : processText(el) } } }, @@ -855,13 +859,20 @@ function modifyAttr (el, name, val) { } } -function postMoveBaseDirective (target, source, isDelete = true) { +function postMoveBaseDirective (target, source, options, isDelete = true) { target.for = source.for target.if = source.if target.elseif = source.elseif target.else = source.else - postProcessFor(target) - postProcessIf(target) + + if (options.runtimeCompile) { + postProcessForDynamic(target, config[mode]) + postProcessIfDynamic(target, config[mode]) + } else { + postProcessFor(target) + postProcessIf(target) + } + if (isDelete) { delete source.for delete source.if @@ -969,7 +980,7 @@ function processComponentIs (el, options) { const eventIdentifier = '__mpx_event__' -function parseFuncStr (str) { +function parseFuncStr (str, extraStr = '') { const funcRE = /^([^()]+)(\((.*)\))?/ const match = funcRE.exec(str) if (match) { @@ -987,7 +998,7 @@ function parseFuncStr (str) { } return { hasArgs, - expStr: `[${funcName + args}]` + expStr: `[${funcName + args + extraStr}]` } } } @@ -1040,7 +1051,10 @@ function processEvent (el, options) { if (parsedEvent) { const type = parsedEvent.eventName const modifiers = (parsedEvent.modifier || '').split('.') - const parsedFunc = parseFuncStr(value) + const prefix = parsedEvent.prefix + // catch 场景下,下发的 eventconfig 里面包含特殊字符,用以运行时的判断 + const extraStr = options.runtimeCompile && prefix === 'catch' ? `, "__mpx_${prefix}"` : '' + const parsedFunc = parseFuncStr(value, extraStr) if (parsedFunc) { if (!eventConfigMap[type]) { eventConfigMap[type] = { @@ -1741,10 +1755,13 @@ function processText (el, meta) { // }]) // } -function injectWxs (meta, module, src) { +function injectWxs (meta, module, src, options) { if (addWxsModule(meta, module, src)) { return } + if (options && options.runtimeCompile) { + return + } const wxsNode = createASTElement(config[mode].wxs.tag, [ { name: config[mode].wxs.module, @@ -1947,7 +1964,7 @@ function processBuiltInComponents (el, meta) { } } -function postProcessAliComponentRootView (el, options) { +function postProcessAliComponentRootView (el, options, meta) { const processAttrsConditions = [ { condition: /^(on|catch)Tap$/, action: 'clone' }, { condition: /^(on|catch)TouchStart$/, action: 'clone' }, @@ -2001,9 +2018,16 @@ function postProcessAliComponentRootView (el, options) { processAppendRules(el) const componentWrapView = createASTElement('view', newAttrs) + replaceNode(el, componentWrapView, true) addChild(componentWrapView, el) - postMoveBaseDirective(componentWrapView, el) + processAttrs(componentWrapView, options) + postMoveBaseDirective(componentWrapView, el, options) + + if (options.runtimeCompile) { + collectDynamicInfo(componentWrapView, options, meta) + postProcessAttrsDynamic(componentWrapView, config[mode]) + } } // 有virtualHost情况wx组件注入virtualHost。无virtualHost阿里组件注入root-view。其他跳过。 @@ -2063,22 +2087,30 @@ function processShow (el, options, root) { value: show }]) } else { - processShowStyle() + if (options.runtimeCompile) { + processShowStyleDynamic(el, show) + } else { + processShowStyle(el, show) + } } } else { - processShowStyle() + if (options.runtimeCompile) { + processShowStyleDynamic(el, show) + } else { + processShowStyle(el, show) + } } +} - function processShowStyle () { - if (show !== undefined) { - const showExp = parseMustacheWithContext(show).result - let oldStyle = getAndRemoveAttr(el, 'style').val - oldStyle = oldStyle ? oldStyle + ';' : '' - addAttrs(el, [{ - name: 'style', - value: `${oldStyle}{{${showExp}?'':'display:none;'}}` - }]) - } +function processShowStyle (el, show) { + if (show !== undefined) { + const showExp = parseMustacheWithContext(show).result + let oldStyle = getAndRemoveAttr(el, 'style').val + oldStyle = oldStyle ? oldStyle + ';' : '' + addAttrs(el, [{ + name: 'style', + value: `${oldStyle}{{${showExp}?'':'display:none;'}}` + }]) } } @@ -2201,11 +2233,11 @@ function processDuplicateAttrsList (el) { } // 处理wxs注入逻辑 -function processInjectWxs (el, meta) { +function processInjectWxs (el, meta, options) { if (el.injectWxsProps && el.injectWxsProps.length) { el.injectWxsProps.forEach((injectWxsProp) => { const { injectWxsPath, injectWxsModuleName } = injectWxsProp - injectWxs(meta, injectWxsModuleName, injectWxsPath) + injectWxs(meta, injectWxsModuleName, injectWxsPath, options) }) } } @@ -2243,7 +2275,7 @@ function processElement (el, root, options, meta) { processMpxTagName(el) - processInjectWxs(el, meta) + processInjectWxs(el, meta, options) const transAli = mode === 'ali' && srcMode === 'wx' @@ -2271,8 +2303,13 @@ function processElement (el, root, options, meta) { processIf(el) processFor(el) processRef(el, options, meta) - processClass(el, meta) - processStyle(el, meta) + if (options.runtimeCompile) { + processClassDynamic(el, meta) + processStyleDynamic(el, meta) + } else { + processClass(el, meta) + processStyle(el, meta) + } processEvent(el, options) if (!pass) { @@ -2285,6 +2322,8 @@ function processElement (el, root, options, meta) { function closeElement (el, meta, options) { postProcessAtMode(el) + collectDynamicInfo(el, options, meta) + if (mode === 'web') { postProcessWxs(el, meta) // 处理代码维度条件编译移除死分支 @@ -2295,12 +2334,24 @@ function closeElement (el, meta, options) { postProcessWxs(el, meta) if (!pass) { if (isComponentNode(el, options) && !hasVirtualHost && mode === 'ali') { - postProcessAliComponentRootView(el, options) + postProcessAliComponentRootView(el, options, meta) } - postProcessComponentIs(el) + postProcessComponentIs(el, options) + } + + if (options.runtimeCompile) { + postProcessForDynamic(el, config[mode]) + postProcessIfDynamic(el, config[mode]) + postProcessAttrsDynamic(el, config[mode]) + } else { + postProcessFor(el) + postProcessIf(el) } - postProcessFor(el) - postProcessIf(el) +} + +// 运行时组件的模版节点收集,最终注入到 mpx-custom-element-*.wxml 中 +function collectDynamicInfo (el, options, meta) { + setBaseWxml(el, { mode, isComponentNode, options }, meta) } function postProcessAtMode (el) { @@ -2331,7 +2382,7 @@ function cloneAttrsList (attrsList) { }) } -function postProcessComponentIs (el) { +function postProcessComponentIs (el, options) { if (el.is && el.components) { let tempNode if (el.for || el.if || el.elseif || el.else) { @@ -2340,7 +2391,7 @@ function postProcessComponentIs (el) { tempNode = getTempNode() } replaceNode(el, tempNode, true) - postMoveBaseDirective(tempNode, el) + postMoveBaseDirective(tempNode, el, options) el.components.forEach(function (component) { const newChild = createASTElement(component, cloneAttrsList(el.attrsList), tempNode) @@ -2550,6 +2601,150 @@ function genNode (node) { return exp } +function addIfConditionDynamic (el, condition) { + if (!el.ifConditions) { + el.ifConditions = [] + } + el.ifConditions.push(condition) +} + +function processIfConditionsDynamic (el) { + const prevNode = findPrevNode(el) + if (prevNode && prevNode.if) { + addIfConditionDynamic(prevNode, { + ifExp: !!el.elseif, + block: el, + __exp: el.elseif ? parseExp(el.elseif.exp) : '' + }) + removeNode(el) + } +} + +function processClassDynamic (el, meta) { + const type = 'class' + const targetType = type + const dynamicClass = getAndRemoveAttr(el, config[mode].directive.dynamicClass).val + let staticClass = getAndRemoveAttr(el, type).val || '' + staticClass = staticClass.replace(/\s+/g, ' ') + if (dynamicClass) { + const staticClassExp = parseMustacheWithContext(staticClass).result + const dynamicClassExp = transDynamicClassExpr(parseMustacheWithContext(dynamicClass).result, { + error: error$1 + }) + addAttrs(el, [{ + name: targetType, + value: `{{[${staticClassExp},${dynamicClassExp}]}}` + }]) + } else if (staticClass) { + addAttrs(el, [{ + name: targetType, + value: staticClass + }]) + } +} + +function processStyleDynamic (el, meta) { + const type = 'style' + const targetType = type + const dynamicStyle = getAndRemoveAttr(el, config[mode].directive.dynamicStyle).val + let staticStyle = getAndRemoveAttr(el, type).val || '' + staticStyle = staticStyle.replace(/\s+/g, ' ') + if (dynamicStyle) { + const staticStyleExp = parseMustacheWithContext(staticStyle).result + const dynamicStyleExp = parseMustacheWithContext(dynamicStyle).result + addAttrs(el, [{ + name: targetType, + value: `{{[${staticStyleExp},${dynamicStyleExp}]}}` + }]) + } else if (staticStyle) { + addAttrs(el, [{ + name: targetType, + value: staticStyle + }]) + } +} + +function processTextDynamic (vnode) { + if (vnode.type !== 3 || vnode.isComment) { + return + } + const parsed = parseMustacheWithContext(vnode.text) + if (parsed.hasBinding) { + vnode.__exp = parseExp(parsed.result) + delete vnode.text + } +} + +function postProcessIfDynamic (vnode, config) { + if (vnode.if) { + const parsedExp = vnode.if.exp + addIfConditionDynamic(vnode, { + ifExp: true, + block: 'self', + __exp: parseExp(parsedExp) + }) + getAndRemoveAttr(vnode, config.directive.if) + vnode.if = true + } else if (vnode.elseif || vnode.else) { + const directive = vnode.elseif + ? config.directive.elseif + : config.directive.else + getAndRemoveAttr(vnode, directive) + processIfConditionsDynamic(vnode) + delete vnode.elseif + delete vnode.else + } +} + +function postProcessForDynamic (vnode) { + if (vnode.for) { + vnode.for.__exp = parseExp(vnode.for.exp) + delete vnode.for.raw + delete vnode.for.exp + popForScopes() + } +} + +function postProcessAttrsDynamic (vnode, config) { + const exps = vnode.exps?.filter(v => v.attrName) || [] + const expsMap = Object.fromEntries(exps.map(v => ([v.attrName, v]))) + const directives = Object.values(config.directive) + if (vnode.attrsList && vnode.attrsList.length) { + // 后序遍历,主要为了做剔除的操作 + for (let i = vnode.attrsList.length - 1; i >= 0; i--) { + const attr = vnode.attrsList[i] + if (config.event.parseEvent(attr.name) || directives.includes(attr.name)) { + // 原本的事件代理直接剔除,主要是基础模版的事件直接走代理形式,事件绑定名直接写死的,优化 astJson 体积 + getAndRemoveAttr(vnode, attr.name) + } else if (attr.value == null) { + attr.__exp = parseExp('true') + } else { + const expInfo = expsMap[attr.name] + if (expInfo && expInfo.exp) { + attr.__exp = parseExp(expInfo.exp) + } + } + if (attr.__exp) { + delete attr.value + } + } + } +} + +function processShowStyleDynamic (el, show) { + if (show !== undefined) { + const showExp = parseMustacheWithContext(show).result + const oldStyle = getAndRemoveAttr(el, 'style').val + const displayExp = `${showExp}? '' : "display:none;"` + const isArray = oldStyle?.endsWith(']}}') + const value = isArray ? oldStyle?.replace(']}}', `,${displayExp}]}}`) : `${oldStyle ? `${oldStyle};` : ''}{{${displayExp}}}` + addAttrs(el, [{ + name: 'style', + value: value + }]) + } +} + module.exports = { parseComponent, parse, @@ -2560,5 +2755,10 @@ module.exports = { parseMustache, parseMustacheWithContext, stringifyWithResolveComputed, - addAttrs + addAttrs, + getAndRemoveAttr, + findPrevNode, + removeNode, + replaceNode, + createASTElement } diff --git a/packages/webpack-plugin/lib/template-compiler/dynamic.js b/packages/webpack-plugin/lib/template-compiler/dynamic.js new file mode 100644 index 0000000000..84a04ecdb1 --- /dev/null +++ b/packages/webpack-plugin/lib/template-compiler/dynamic.js @@ -0,0 +1,13 @@ +const uselessAttrs = ['parent', 'exps', 'unary', 'attrsMap'] +const uselessArrAttrs = ['children', 'attrsList'] + +function stringify (ast) { + return JSON.stringify(ast, (k, v) => { + if (uselessAttrs.includes(k)) return undefined + if (uselessArrAttrs.includes(k) && v && !v.length) return undefined + if (k === 'tag' && v === 'temp-node') return 'block' + return v + }) +} + +module.exports.stringify = stringify diff --git a/packages/webpack-plugin/lib/template-compiler/index.js b/packages/webpack-plugin/lib/template-compiler/index.js index 5eb6cacbdb..e8148558e4 100644 --- a/packages/webpack-plugin/lib/template-compiler/index.js +++ b/packages/webpack-plugin/lib/template-compiler/index.js @@ -3,6 +3,10 @@ const bindThis = require('./bind-this') const parseRequest = require('../utils/parse-request') const { matchCondition } = require('../utils/match-condition') const loaderUtils = require('loader-utils') +const { MPX_DISABLE_EXTRACTOR_CACHE } = require('../utils/const') +const RecordRuntimeInfoDependency = require('../dependencies/RecordRuntimeInfoDependency') +const { createTemplateEngine, createSetupTemplate } = require('@mpxjs/template-engine') +const { stringify } = require('./dynamic') module.exports = function (raw) { this.cacheable() @@ -17,6 +21,7 @@ module.exports = function (raw) { const decodeHTMLText = mpx.decodeHTMLText const globalSrcMode = mpx.srcMode const localSrcMode = queryObj.mode + const packageName = queryObj.packageRoot || mpx.currentPackageRoot || 'main' const wxsContentMap = mpx.wxsContentMap const optimizeRenderRules = mpx.optimizeRenderRules const usingComponents = queryObj.usingComponents || [] @@ -25,6 +30,7 @@ module.exports = function (raw) { const isNative = queryObj.isNative const ctorType = queryObj.ctorType const hasScoped = queryObj.hasScoped + const runtimeCompile = queryObj.isDynamic const moduleId = queryObj.moduleId || '_' + mpx.pathHash(resourcePath) let optimizeRenderLevel = 0 @@ -49,6 +55,7 @@ module.exports = function (raw) { const { root: ast, meta } = compiler.parse(raw, { warn, error, + runtimeCompile, usingComponents, componentPlaceholder, hasComment, @@ -67,7 +74,7 @@ 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) }) @@ -77,7 +84,7 @@ module.exports = function (raw) { } } - const result = compiler.serialize(ast) + let result = runtimeCompile ? '' : compiler.serialize(ast) if (isNative) { return result @@ -95,7 +102,11 @@ global.currentInject = { moduleId: ${JSON.stringify(moduleId)} };\n` - const rawCode = compiler.genNode(ast) + if (runtimeCompile || queryObj.dynamicRuntime) { + resultSource += 'global.currentInject.dynamic = true;\n' + } + + const rawCode = runtimeCompile ? '' : compiler.genNode(ast) if (rawCode) { try { const ignoreMap = Object.assign({ @@ -157,5 +168,26 @@ global.currentInject.getRefsData = function () { extractedResultSource: resultSource }) + if (queryObj.mpxCustomElement) { + this.cacheable(false) + const templateEngine = createTemplateEngine(mpx.mode) + result += `${createSetupTemplate()}\n` + templateEngine.buildTemplate(mpx.getPackageInjectedTemplateConfig(packageName)) + } + + // 运行时编译的组件直接返回基础模板的内容,并产出动态文本内容 + if (runtimeCompile) { + // 包含了运行时组件的template模块必须每次都创建(但并不是每次都需要build),用于收集组件节点信息,传递信息以禁用父级extractor的缓存 + this.emitFile(MPX_DISABLE_EXTRACTOR_CACHE, '', undefined, { skipEmit: true }) + + const templateInfo = { + templateAst: stringify(ast), + ...meta.runtimeInfo + } + + // 以 package 为维度存储,meta 上的数据也只是存储了这个组件的 template 上获取的信息,需要在 dependency 里面再次进行合并操作 + this._module.addPresentationalDependency(new RecordRuntimeInfoDependency(packageName, resourcePath, { type: 'template', info: templateInfo })) + // 运行时组件的模版直接返回空,在生成模版静态文件的时候(beforeModuleAssets)再动态注入 + } + 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..1c1210f3d2 --- /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: 5 }), visitor) + }, + NODE_TYPE +} 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..c3daca64ee 100644 --- a/packages/webpack-plugin/lib/wxml/loader.js +++ b/packages/webpack-plugin/lib/wxml/loader.js @@ -5,6 +5,7 @@ const config = require('../config') const createHelpers = require('../helpers') const isUrlRequest = require('../utils/is-url-request') const parseRequest = require('../utils/parse-request') +const genDynamicTemplate = require('../runtime-render/gen-dynamic-template') let count = 0 @@ -26,6 +27,14 @@ module.exports = function (content) { const mode = mpx.mode const localSrcMode = queryObj.mode const customAttributes = options.attributes || mpx.attributes || [] + const packageName = queryObj.packageRoot || mpx.currentPackageRoot || 'main' + const isDynamic = queryObj.isDynamic + + const exportsString = 'module.exports = ' + + if (isDynamic) { + return exportsString + JSON.stringify(genDynamicTemplate(packageName)) + ';' + } const { getRequestString } = createHelpers(this) @@ -71,8 +80,6 @@ module.exports = function (content) { content = content.join('') content = JSON.stringify(content) - const exportsString = 'module.exports = ' - return exportsString + content.replace(/__HTMLLINK__\d+__/g, (match) => { if (!data[match]) return match diff --git a/packages/webpack-plugin/package.json b/packages/webpack-plugin/package.json index 8cdfe00502..70cfffb198 100644 --- a/packages/webpack-plugin/package.json +++ b/packages/webpack-plugin/package.json @@ -27,7 +27,9 @@ "@better-scroll/slide": "^2.5.1", "@better-scroll/wheel": "^2.5.1", "@better-scroll/zoom": "^2.5.1", + "@mpxjs/template-engine": "^2.8.7", "acorn-walk": "^7.2.0", + "acorn": "^8.11.3", "async": "^2.6.0", "css": "^2.2.1", "css-selector-tokenizer": "^0.7.0", diff --git a/test/e2e/miniprogram-project/package.json b/test/e2e/miniprogram-project/package.json index 2bb89f67b6..41752db03a 100644 --- a/test/e2e/miniprogram-project/package.json +++ b/test/e2e/miniprogram-project/package.json @@ -42,6 +42,7 @@ "@mpxjs/miniprogram-simulate": "1.4.9", "@mpxjs/mpx-jest": "0.0.27", "@mpxjs/webpack-plugin": "^2.7.2", + "@mpxjs/template-engine": "^2.8.7", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "babel-eslint": "^10.0.1", diff --git a/test/e2e/miniprogram-wxss-loader/package.json b/test/e2e/miniprogram-wxss-loader/package.json index 3f63814b8f..2263a398d8 100644 --- a/test/e2e/miniprogram-wxss-loader/package.json +++ b/test/e2e/miniprogram-wxss-loader/package.json @@ -44,6 +44,7 @@ "@mpxjs/miniprogram-simulate": "1.4.9", "@mpxjs/mpx-jest": "0.0.27", "@mpxjs/webpack-plugin": "^2.7.55", + "@mpxjs/template-engine": "^2.8.7", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "babel-eslint": "^10.0.1", diff --git a/test/e2e/plugin-project/package.json b/test/e2e/plugin-project/package.json index a4d6f7f912..9b14f1420f 100644 --- a/test/e2e/plugin-project/package.json +++ b/test/e2e/plugin-project/package.json @@ -43,6 +43,7 @@ "@mpxjs/miniprogram-simulate": "^1.4.17", "@mpxjs/mpx-jest": "0.0.27", "@mpxjs/webpack-plugin": "^2.7.2", + "@mpxjs/template-engine": "^2.8.7", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "autoprefixer": "^6.3.1",