diff --git a/packages/core/src/platform/patch/web/getDefaultOptions.js b/packages/core/src/platform/patch/web/getDefaultOptions.js index 7d53c10917..256420c302 100644 --- a/packages/core/src/platform/patch/web/getDefaultOptions.js +++ b/packages/core/src/platform/patch/web/getDefaultOptions.js @@ -66,7 +66,12 @@ export function getDefaultOptions ({ type, rawOptions = {} }) { } } const rootMixins = [{ + data: { + _data_v_id: '' // template 透传scoped样式用 + }, beforeCreate () { + const _scopeId = this.$vnode.componentOptions.Ctor?.options?._scopeId + this._data_v_id = _scopeId initProxy(this, rawOptions) }, created () { diff --git a/packages/webpack-plugin/lib/dependencies/WriteVfsDependency.js b/packages/webpack-plugin/lib/dependencies/WriteVfsDependency.js new file mode 100644 index 0000000000..851030fff1 --- /dev/null +++ b/packages/webpack-plugin/lib/dependencies/WriteVfsDependency.js @@ -0,0 +1,46 @@ +const NullDependency = require('webpack/lib/dependencies/NullDependency') +const makeSerializable = require('webpack/lib/util/makeSerializable') + +class WriteVfsDependency extends NullDependency { + constructor (filename, content) { + super() + this.filename = filename + this.content = content + } + + get type () { + return 'mpx app entry' + } + + mpxAction (module, compilation, callback) { + const mpx = compilation.__mpx__ + const vfs = mpx.__vfs + if (vfs) { + vfs.writeModule(this.filename, this.content) + } + return callback() + } + + serialize (context) { + const { write } = context + write(this.filename) + write(this.content) + super.serialize(context) + } + + deserialize (context) { + const { read } = context + this.filename = read() + this.content = read() + super.deserialize(context) + } +} + +WriteVfsDependency.Template = class WriteVfsDependencyTemplate { + apply () { + } +} + +makeSerializable(WriteVfsDependency, '@mpxjs/webpack-plugin/lib/dependencies/WriteVfsDependency') + +module.exports = WriteVfsDependency diff --git a/packages/webpack-plugin/lib/index.js b/packages/webpack-plugin/lib/index.js index b45629adcf..db5b2a6a60 100644 --- a/packages/webpack-plugin/lib/index.js +++ b/packages/webpack-plugin/lib/index.js @@ -42,6 +42,7 @@ const FlagPluginDependency = require('./dependencies/FlagPluginDependency') const RemoveEntryDependency = require('./dependencies/RemoveEntryDependency') const RecordLoaderContentDependency = require('./dependencies/RecordLoaderContentDependency') const RecordRuntimeInfoDependency = require('./dependencies/RecordRuntimeInfoDependency') +const WriteVfsDependency = require('./dependencies/WriteVfsDependency') const SplitChunksPlugin = require('webpack/lib/optimize/SplitChunksPlugin') const fixRelative = require('./utils/fix-relative') const parseRequest = require('./utils/parse-request') @@ -623,6 +624,9 @@ class MpxWebpackPlugin { compilation.dependencyFactories.set(RecordRuntimeInfoDependency, new NullFactory()) compilation.dependencyTemplates.set(RecordRuntimeInfoDependency, new RecordRuntimeInfoDependency.Template()) + compilation.dependencyFactories.set(WriteVfsDependency, new NullFactory()) + compilation.dependencyTemplates.set(WriteVfsDependency, new WriteVfsDependency.Template()) + compilation.dependencyTemplates.set(ImportDependency, new ImportDependencyTemplate()) }) diff --git a/packages/webpack-plugin/lib/template-compiler/compiler.js b/packages/webpack-plugin/lib/template-compiler/compiler.js index ac6ac710ce..637e4c4d2c 100644 --- a/packages/webpack-plugin/lib/template-compiler/compiler.js +++ b/packages/webpack-plugin/lib/template-compiler/compiler.js @@ -16,6 +16,7 @@ const setBaseWxml = require('../runtime-render/base-wxml') const { parseExp } = require('./parse-exps') const shallowStringify = require('../utils/shallow-stringify') const { isReact } = require('../utils/env') +const getTemplateContent = require('../utils/get-template-content') const no = function () { return false @@ -671,6 +672,7 @@ function parse (template, options) { root = currentParent = getVirtualHostRoot(options, meta) stack.push(root) } + options.template = template // processTemplate时需要对template(只用于处理含name的情况)做截取 parseHTML(template, { warn: warn$1, @@ -678,6 +680,7 @@ function parse (template, options) { isUnaryTag: options.isUnaryTag, canBeLeftOpenTag: options.canBeLeftOpenTag, shouldKeepComment: true, + wxmlName: options.wxmlName, start: function start (tag, attrs, unary) { // check namespace. // inherit parent ns if there is one @@ -2097,7 +2100,7 @@ function processWebExternalClassesHack (el, options) { if (staticClass) { const classNames = staticClass.split(/\s+/) const replacements = [] - options.externalClasses.forEach((className) => { + options.externalClasses?.forEach((className) => { const index = classNames.indexOf(className) if (index > -1) { replacements.push(`$attrs[${stringify(className)}]`) @@ -2350,8 +2353,33 @@ function processTemplate (el) { } } -function postProcessTemplate (el) { +function processImport (el, options, meta) { + if (el.tag === 'import' && el.attrsMap.src) { + if (!meta.templateSrcList) { + meta.templateSrcList = [] + } + if (!meta.templateSrcList.includes(el.attrsMap.src)) { + meta.templateSrcList.push(el.attrsMap.src) + } + } +} + +function postProcessTemplate (el, meta, options) { if (el.isTemplate) { + if (mode === 'web') { + if (!meta.inlineTemplateMap) { + meta.inlineTemplateMap = {} + } + const name = el.attrsMap.name + if (name) { + const content = getTemplateContent(options.template, name) + const filePath = options.filePath.replace(/.mpx$/, `-${name}.wxml`) + meta.inlineTemplateMap[name] = { + filePath, + content + } + } + } processingTemplate = false return true } @@ -2486,6 +2514,7 @@ function processMpxTagName (el) { } function processElement (el, root, options, meta) { + const initialTag = el.tag // 预存,在这个阶段增加_fakeTemplate值会影响web小程序元素trans web元素 processAtMode(el) // 如果已经标记了这个元素要被清除,直接return跳过后续处理步骤 if (el._atModeStatus === 'mismatch') { @@ -2508,10 +2537,17 @@ function processElement (el, root, options, meta) { const transAli = mode === 'ali' && srcMode === 'wx' if (mode === 'web') { + if (initialTag === 'block') { + el._fakeTemplate = true // 该值是在template2vue中处理block转换的template的情况 + } // 收集内建组件 processBuiltInComponents(el, meta) // 预处理代码维度条件编译 processIfWeb(el) + // 预处理template逻辑 + processTemplate(el) + // 预处理import逻辑 + processImport(el, options, meta) processWebExternalClassesHack(el, options) processComponentGenericsWeb(el, options, meta) return @@ -2569,6 +2605,8 @@ function closeElement (el, meta, options) { if (mode === 'web') { postProcessWxs(el, meta) + // 处理web下template逻辑 + postProcessTemplate(el, meta, options) // 处理代码维度条件编译移除死分支 postProcessIf(el) return @@ -2579,7 +2617,7 @@ function closeElement (el, meta, options) { postProcessIfReact(el) return } - const pass = isNative || postProcessTemplate(el) || processingTemplate + const pass = isNative || postProcessTemplate(el, meta) || processingTemplate postProcessWxs(el, meta) if (!pass) { if (isComponentNode(el, options) && !hasVirtualHost && mode === 'ali') { @@ -2693,9 +2731,28 @@ function serialize (root) { result += node.text } } - if (node.tag === 'wxs' && mode === 'web') { + if ((node.tag === 'wxs' || node.tag === 'import') && mode === 'web') { return result } + if (mode === 'web') { + if (node.tag === 'template' && node.attrsMap && node.attrsMap.name) { + return result + } + if (node.tag === 'template' && node.attrsMap && node.attrsMap.is) { + node.tag = 'component' + node.attrsList.forEach((item) => { + if (item.name === 'is') { + item.name = ':is' + item.value = `'${item.value}'` + } + if (item.name === ':data') { + item.name = 'v-bind' + const bindValue = item.value.replace(/\(|\)/g, '') + item.value = bindValue ? `{${bindValue}, _data_v_id}` : '{ _data_v_id }' // 用于处理父组件scoped情况下template中的样式传递 + } + }) + } + } if (node.type === 1) { if (node.tag !== 'temp-node') { result += '<' + node.tag diff --git a/packages/webpack-plugin/lib/utils/get-template-content.js b/packages/webpack-plugin/lib/utils/get-template-content.js new file mode 100644 index 0000000000..f0cbc395af --- /dev/null +++ b/packages/webpack-plugin/lib/utils/get-template-content.js @@ -0,0 +1,47 @@ +/* + 对template.wxml文件做截取 + @source原始小程序文件 + @name 要匹配的该name的template + */ +module.exports = function (source, name) { + // 使用正则表达式匹配具有 name 的 template 标签及其所有子元素 + // 正则表达式使用非贪婪匹配来递归匹配嵌套的 template + const regex = new RegExp(`(]*\\bname=["|']${name}["|'][^>]*>).*?`, 'g') + + let startIndex = 0 + let endIndex = 0 + const match = regex.exec(source) + // 逐个处理匹配到的 template 标签及其内容 + if (match) { + const matchRes = match[0] + const reg = /<\/?template\s*[^>]*>/g + let n = 0 + startIndex = match.index + endIndex = startIndex + matchRes.length + let html = source.substr(endIndex) + while (html) { + const matchRes = html.match(reg) + if (matchRes.length) { + const matchTemp = matchRes[0] + const matchIndex = html.indexOf(matchTemp) + const matchLength = matchTemp.length + const cutLength = matchIndex + matchLength + if (matchTemp.startsWith('')) { + if (n === 0) { + endIndex += cutLength + break + } else { + n-- + } + } else { + n++ + } + endIndex += cutLength + html = html.substr(cutLength) + } + } + } else { + return '' + } + return source.substring(startIndex, endIndex) +} diff --git a/packages/webpack-plugin/lib/web/index.js b/packages/webpack-plugin/lib/web/index.js index b7c10984d9..48b41797a6 100644 --- a/packages/webpack-plugin/lib/web/index.js +++ b/packages/webpack-plugin/lib/web/index.js @@ -107,6 +107,8 @@ module.exports = function ({ builtInComponentsMap: templateRes.builtInComponentsMap, genericsInfo: templateRes.genericsInfo, wxsModuleMap: templateRes.wxsModuleMap, + templateSrcList: templateRes.templateSrcList, + inlineTemplateMap: templateRes.inlineTemplateMap, localComponentsMap: jsonRes.localComponentsMap }, callback) } diff --git a/packages/webpack-plugin/lib/web/processScript.js b/packages/webpack-plugin/lib/web/processScript.js index 9d4535358e..5977c43c55 100644 --- a/packages/webpack-plugin/lib/web/processScript.js +++ b/packages/webpack-plugin/lib/web/processScript.js @@ -3,6 +3,8 @@ const loaderUtils = require('loader-utils') const normalize = require('../utils/normalize') const shallowStringify = require('../utils/shallow-stringify') const optionProcessorPath = normalize.lib('runtime/optionProcessor') +const wxmlTemplateLoader = normalize.lib('web/wxml-template-loader') +const WriteVfsDependency = require('../dependencies/WriteVfsDependency') const { buildComponentsMap, getRequireScript, @@ -22,18 +24,20 @@ module.exports = function (script, { builtInComponentsMap, genericsInfo, wxsModuleMap, + templateSrcList, + inlineTemplateMap, localComponentsMap }, callback) { - const { projectRoot, appInfo, webConfig } = loaderContext.getMpx() + const { projectRoot, appInfo, webConfig, __vfs: vfs } = loaderContext.getMpx() let output = '/* script */\n' - let scriptSrcMode = srcMode if (script) { scriptSrcMode = script.mode || scriptSrcMode } else { script = { tag: 'script' } } + output += genComponentTag(script, { attrs (script) { const attrs = Object.assign({}, script.attrs) @@ -58,17 +62,35 @@ module.exports = function (script, { content += ` wxsModules.${module} = ${expression}\n` }) } + content += 'const templateModules = {}\n' + if (templateSrcList?.length) { // import标签处理 + templateSrcList?.forEach((item) => { + content += ` + const tempLoaderResult = require(${stringifyRequest(this, `!!${wxmlTemplateLoader}!${item}`)})\n + Object.assign(templateModules, tempLoaderResult)\n` + }) + } // 获取组件集合 const componentsMap = buildComponentsMap({ localComponentsMap, builtInComponentsMap, loaderContext, jsonConfig }) - // 获取pageConfig const pageConfig = {} if (ctorType === 'page') { Object.assign(pageConfig, jsonConfig) delete pageConfig.usingComponents } - + if (inlineTemplateMap) { // 处理行内template(只有属性为name的情况) + const inlineTemplateMapLists = Object.keys(inlineTemplateMap) + if (inlineTemplateMapLists.length) { + inlineTemplateMapLists.forEach((name) => { + const { filePath, content } = inlineTemplateMap[name] + loaderContext._module.addPresentationalDependency(new WriteVfsDependency(filePath, content)) // 处理缓存报错的情况 + vfs.writeModule(filePath, content) // 截取template写入文件 + const expression = `getComponent(require(${stringifyRequest(loaderContext, `${filePath}?is=${name}&isTemplate`)}))` + componentsMap[name] = expression + }) + } + } content += buildGlobalParams({ moduleId, scriptSrcMode, loaderContext, isProduction, webConfig, hasApp }) content += getRequireScript({ ctorType, script, loaderContext }) content += ` @@ -78,7 +100,7 @@ module.exports = function (script, { outputPath: ${JSON.stringify(outputPath)}, pageConfig: ${JSON.stringify(pageConfig)}, // @ts-ignore - componentsMap: ${shallowStringify(componentsMap)}, + componentsMap: Object.assign(${shallowStringify(componentsMap)}, templateModules), componentGenerics: ${JSON.stringify(componentGenerics)}, genericsInfo: ${JSON.stringify(genericsInfo)}, wxsMixin: getWxsMixin(wxsModules), @@ -87,7 +109,6 @@ module.exports = function (script, { return content } }) - callback(null, { output }) diff --git a/packages/webpack-plugin/lib/web/processTemplate.js b/packages/webpack-plugin/lib/web/processTemplate.js index de5fea1485..b89cb4e332 100644 --- a/packages/webpack-plugin/lib/web/processTemplate.js +++ b/packages/webpack-plugin/lib/web/processTemplate.js @@ -30,7 +30,7 @@ module.exports = function (template, { const { resourcePath } = parseRequest(loaderContext.resource) const builtInComponentsMap = {} - let wxsModuleMap, genericsInfo + let wxsModuleMap, genericsInfo, inlineTemplateMap, templateSrcList let output = '/* template */\n' if (ctorType === 'app') { @@ -103,6 +103,12 @@ module.exports = function (template, { wxsContentMap[`${resourcePath}~${module}`] = meta.wxsContentMap[module] } } + if (meta.inlineTemplateMap) { + inlineTemplateMap = meta.inlineTemplateMap + } + if (meta.templateSrcList?.length) { + templateSrcList = meta.templateSrcList + } if (meta.builtInComponentsMap) { Object.keys(meta.builtInComponentsMap).forEach((name) => { builtInComponentsMap[name] = { @@ -118,10 +124,11 @@ module.exports = function (template, { }) output += '\n' } - callback(null, { output, builtInComponentsMap, + inlineTemplateMap, + templateSrcList, genericsInfo, wxsModuleMap }) diff --git a/packages/webpack-plugin/lib/web/template2vue.js b/packages/webpack-plugin/lib/web/template2vue.js new file mode 100644 index 0000000000..c6d0c948bc --- /dev/null +++ b/packages/webpack-plugin/lib/web/template2vue.js @@ -0,0 +1,247 @@ +const templateCompiler = require('../template-compiler/compiler') +const parseRequest = require('../utils/parse-request') +const addQuery = require('../utils/add-query') +const { buildComponentsMap } = require('./script-helper') +const normalize = require('../utils/normalize') +const optionProcessorPath = normalize.lib('runtime/optionProcessor') +const shallowStringify = require('../utils/shallow-stringify') +const { stringifyRequest } = require('./script-helper') +const parseQuery = require('loader-utils').parseQuery +const wxmlTemplateLoader = normalize.lib('web/wxml-template-loader') + +const getRefVarNames = function (str) { // 获取元素属性上用到的动态数据keyname + const regex = /\(([a-zA-Z_$]\w*)\)/g + + const matches = str.match(regex) || [] + const variableNames = matches.map(name => { + const getName = /\((\w+)\)/.exec(name) + return getName[1] + }) + return variableNames +} + +const getEventName = function (eventStr) { // 获取事件用到的动态数据keyname + const regex = /\b(\w+)\s*\(([^)]*?)\)/g + const regexInParams = /\b(? { + this.emitWarning( + new Error('[template compiler][' + this.resource + ']: ' + msg) + ) + }, + error: (msg) => { + this.emitError( + new Error('[template compiler][' + this.resource + ']: ' + msg) + ) + }, + mode: 'web', + srcMode: 'wx', + wxmlName: query.is, + filePath: resourcePath, + usingComponents: [] + }) + const builtInComponentsMap = {} + if (meta.builtInComponentsMap) { + Object.keys(meta.builtInComponentsMap).forEach((name) => { + builtInComponentsMap[name] = { + resource: addQuery(meta.builtInComponentsMap[name], { isComponent: true }) + } + }) + } + + const getForValue = function (str) { // 获取v-for中遍历的子对象 + const regex = /\(([^)]+)\)/ + let forValue + const matches = regex.exec(str) + if (matches) { + const matchMaps = matches[1].split(',') + forValue = matchMaps[0] + } + return forValue + } + + function parseText (text, node) { // 拼接数据时过滤一下props + const tagRE = /\{\{((?:.|\r?\n)+?)\}\}/g + if (!tagRE.test(text)) { + return text + } + let forValue + let parent = node.parent + while (parent) { + const value = parent.attrsMap['v-for'] + if (value) { + forValue = getForValue(value) + break + } + parent = parent.parent + } + const tokens = [] + const rawTokens = [] + let lastIndex = (tagRE.lastIndex = 0) + let match, index, tokenValue + while ((match = tagRE.exec(text))) { + index = match.index + // push text token + if (index > lastIndex) { + rawTokens.push((tokenValue = text.slice(lastIndex, index))) + tokens.push(tokenValue) + } + // tag token + const exp = match[1].trim() + if (exp !== forValue) { + props.push(exp) + } + lastIndex = index + match[0].length + } + return text + } + const eventReg = /^@[a-zA-Z]+/ + let isFindRoot = false + const componentNames = [] // 记录引用多少个组件 + function serialize (root) { + function walk (node) { + let result = '' + if (node) { + if (node.type === 3) { + if (node.isComment) { + result += '' + } else { + result += parseText(node.text, node) + } + } + if (node.type === 1) { + if (node.tag === 'wxs' || node.tag === 'import') { // wxml文件里不支持import wxs后续支持 + return '' + } else if (node.tag !== 'temp-node') { + if (node.tag === 'template' && !node._fakeTemplate) { + if (node.attrsMap.name) { // template name处理逻辑 + if (isFindRoot) { + componentNames.push(node.attrsMap.name) + return '' + } + isFindRoot = true + result += '<' + node.tag + } else if (node.attrsMap.is) { // template is处理逻辑 + node.tag = 'component' + result += '<' + node.tag + node.attrsList.forEach((item) => { + if (item.name === 'is') { + item.name = ':is' + item.value = `'${item.value}'` + } + if (item.name === ':data') { + // const bindValue = item.value.replace(/\(([^()]+)\)/, '$1') + item.name = 'v-bind' + // item.value = item.value.replace(/\(([^()]+)\)/, '{$1}') + const bindValue = item.value.replace(/\(|\)/g, '') + item.value = bindValue ? `{${bindValue}, _data_v_id}` : '{ _data_v_id }' + // 获取props 清掉传入是写的空格 + props.push(...bindValue.split(',').map((item) => item?.trim())) + } + result += ' ' + item.name + const value = item.value + if (value != null) { + result += '=' + templateCompiler.stringifyAttr(value) + } + }) + } else { // 其他template逻辑全部丢弃 + return '' + } + } else { + result += '<' + node.tag + ' :[_data_v_id]="_data_v_id"' + let forValue + let tagProps = [] + node.attrsList.forEach(function (attr) { + result += ' ' + attr.name + const value = attr.value + if (attr.name === 'v-for') { + forValue = getForValue(attr.value) + } + if (eventReg.exec(attr.name)) { // 事件 + const result = getEventName(attr.value) + tagProps.push(...result) + } else { // 属性 + const result = getRefVarNames(value) + tagProps.push(...result) + } + if (value != null) { + result += '=' + templateCompiler.stringifyAttr(value) + } + }) + if (forValue) { + tagProps = tagProps.filter((item) => { + return item !== forValue + }) + } + props.push(...tagProps) + } + if (node.unary) { + result += '/>' + } else { + result += '>' + node.children.forEach(function (child) { + result += walk(child) + }) + result += '' + } + } else { + node.children.forEach(function (child) { + result += walk(child) + }) + } + } + } + return result + } + return walk(root) + } + const tempCompMaps = [] + const path = this.resourcePath || '' + const template = serialize(root) + componentNames.forEach((item) => { + tempCompMaps[item] = { + resource: `${path.replace(query.is, item)}?is=${item}&isTemplate` + } + }) + const componentsMap = buildComponentsMap({ localComponentsMap: tempCompMaps, builtInComponentsMap, loaderContext: this, jsonConfig: {} }) + let script = `\n` + const text = template + script + if (query.type === 'template') { + return template + } else if (query.type === 'script') { + return script + } else { + return text + } +} diff --git a/packages/webpack-plugin/lib/web/wxml-template-loader.js b/packages/webpack-plugin/lib/web/wxml-template-loader.js new file mode 100644 index 0000000000..c4fe1aabda --- /dev/null +++ b/packages/webpack-plugin/lib/web/wxml-template-loader.js @@ -0,0 +1,29 @@ +const normalize = require('@mpxjs/webpack-plugin/lib/utils/normalize') +const optionProcessorPath = normalize.lib('runtime/optionProcessor') +const shallowStringify = require('../utils/shallow-stringify') +const getTemplateContent = require('../utils/get-template-content') +const { stringifyRequest } = require('@mpxjs/webpack-plugin/lib/web/script-helper') +const WriteVfsDependency = require('../dependencies/WriteVfsDependency') + +module.exports = function (content) { + const regex = /]*\sname\s*=\s*"([^"]*)"[^>]*>/g + const mpx = this.getMpx() + let match + const templateNames = [] + + while ((match = regex.exec(content)) !== null) { + templateNames.push(match[1]) + } + const templateMaps = {} + templateNames.forEach((name, index) => { + const cutContent = getTemplateContent(content, name) + const resourcePath = this.resourcePath.replace(/.wxml$/, `-${name}.wxml`) + this._module.addPresentationalDependency(new WriteVfsDependency(resourcePath, cutContent)) + mpx.__vfs.writeModule(resourcePath, cutContent) + templateMaps[name] = `getComponent(require(${stringifyRequest(this, `${resourcePath}?is=${name}&isTemplate`)}))` + }) + return ` + const {getComponent} = require(${stringifyRequest(this, optionProcessorPath)})\n + module.exports = ${shallowStringify(templateMaps)} + ` +}