Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

多页程序里面,miniprogram-app、每个入口页为webpack的entry,module内部的单例变量机制失效,如何破? #71

Open
huadong opened this issue Jan 6, 2020 · 25 comments

Comments

@huadong
Copy link

huadong commented Jan 6, 2020

使用webpack进行编译,对于多页应用,miniprogram-app、每页一个入口点,打包后module的单例变量机制失效。

这导致多页程序的页面之间完全隔离,如同浏览器打开了两个独立的窗口(比tab窗口还独立),不仅如此,目前cookie的写法,page之间无法共享cookie。因此:

  1. page之间对js模块的引用和原生引用机制不同,js的module里的单例变量机制失效。
  2. page之间的cookie完全不可见,这和浏览器多tab页机制也不一样。

上边两点约束,kbone基本无法用于多页小程序开发,这个如何破 @JuneAndGreen

@huadong huadong changed the title 多页程序里面,每个pageId对应一个new window(),导致多页间module变量完全隔离,这个如何破? 多页程序里面,miniprogram-app、每个入口页为webpack的entry,module内部的单例变量机制失效,如何破? Jan 7, 2020
@JuneAndGreen
Copy link
Collaborator

page之间对js模块的引用和原生引用机制不同,js的module里的单例变量机制失效。

这个不是很理解,可以给个具体的场景例子么?

page之间的cookie完全不可见,这和浏览器多tab页机制也不一样。

这个目前确实是按页面隔离的,我记一下,后面支持下各个页面的 cookie 互通。

@huadong
Copy link
Author

huadong commented Jan 8, 2020

page之间对js模块的引用和原生引用机制不同,js的module里的单例变量机制失效。

这个不是很理解,可以给个具体的场景例子么?

具体的例子就是:
a.js

// a.js
const a = {hi: 'hello world'}
export default a;

app.js

// app.js
import a from './a'

console.log('app:', a)
a.hi = 'hi~'
console.log('app:', a)

pageA.js

import a from './a'

console.log('page:',  a) // output: hi~

列如目前的vuex是页面级的,而无法做到全局的,因此没有全局的状态管理机制。
因为每个webpack的入口点为一个page,而每个page对应一个webpack的runtime和window,尤其这个webpack的runtime,使得module里的单例在每个webpack的runtime中都会重新初始化。

是否可以考虑全局一个webpack的runtime?
比如在编译后生成的app.js:

function jsonpcreateAppPush(data){
    let found = false;
    for (let index = 0; index < this.length; index++) {
        const e = this[index];
        if (e[0][0] === data[0][0]) {
            found = true;
            break;
        }
    }
    if (!found && oldJsonpcreateAppPush) {
        oldJsonpcreateAppPush.call(this, data);
    }
}
const webpackJsonpcreateApp = [];
var oldJsonpcreateAppPush = webpackJsonpcreateApp.push;
webpackJsonpcreateApp.push = jsonpcreateAppPush.bind(webpackJsonpcreateApp);
const fakeWindow = { webpackJsonpcreateApp };

// ...

App({
    globalData: { webpackJsonpcreateApp },
   // ...
})

其它生成的page.js:

       this.window.webpackJsonpcreateApp = getApp().globalData.webpackJsonpcreateApp;

类似上面这样。目前如果用上述的方式,page二次打开会有点问题,可能跟router有关,还没有具体调试。对vue的runtime不太了解,不知道全局一个webpack的runtime能否支持。

上述方式对web没有什么破坏。小程序JavaScript里就一个运行时,目前webpack的编译生成多入口,多个runtime,实际上会导致page与page、以及app之间的js module隔离。

@JuneAndGreen
Copy link
Collaborator

@huadong 但是页面间的设计本身 runtime 应该隔离的,就如同浏览器中不同 tab 一样,小程序页面本身的设计也是如此,全局数据通常也是通过 getApp 来进行共享,不然页面间可能会有互相干扰出现奇怪的问题。

所以你的需求其实是需要页面间通信的方案?

@huadong
Copy link
Author

huadong commented Jan 9, 2020

这是一个非常值得讨论的问题。从目前微信小程序的开发规范来看,整个小程序是一个JS的运行时,对于module的引用遵从js的规则,详见:模块化。此规则的存在,让大家可以实现全局的状态管理,可以使用redux、vuex之类的。只不过这样去使用,开发者需要谨慎控制页面之间的对象引用关系,但是这样大大提高了代码设计的灵活度和复用性,包括对成熟的开源代码的使用。

不过,对于subpackage的使用,规范有严格的要求:

引用原则
packageA 无法 require packageB JS 文件,但可以 require app、自己 package 内的 JS 文件
packageA 无法 import packageB 的 template,但可以 require app、自己 package 内的 template
packageA 无法使用 packageB 的资源,但可以使用 app、自己 package 内的资源

而目前kbone因为使用webpack独立的runtime机制,进一步增加了隔离,相当于主动削弱了小程序代码支持能力,这会使得kbone的灵活性和扩展性降低,不容易被选用于大规模的生产中。

这里面有个对标问题,小程序是对标H5,还是对标APP。目前原生态小程序可以对标到APP的开发上;而kbone的封装直接对标到了H5上了。kbone从设计思路,代码、文档行文等都非常优秀,但如果仅仅对标到了H5上,就太可惜了。

关于这个问题,#46 里面也提到类似的观点。

@huadong
Copy link
Author

huadong commented Jan 9, 2020

整理一下原生小程序的实际情况:

  • 分包内部(main, packageA, packageA):各页面和js模块可以require自己 package 内的 JS 文件。
  • 分包引用原则:各分包均可require app的js模块。

因此:

  1. 全局数据可以放到main包进行管理,遵从js module的引用规范;
  2. 也可以使用getApp().globalData进行共享。

实际上,kbone在设计上也使用了这个能力,比如cache的管理,小程序全局的config等等。

@JuneAndGreen
Copy link
Collaborator

我明白的你的诉求了,你是希望:假设在页面 A 和页面 B 都引入了 c.js 的情况下,这个 c.js 是运行时也应该是同一个,页面 A 调 c.js 存入的变量在页面 B 是应该是可以正常获取出来的。

不过这里我还是不能将所有页面改成在同一个 webpack 包裹内,kbone 一开始的设计初衷确实是以实现 Web 端写法为目的的,Web 端不同页面的共用模块是隔离的,所以不能偏离这个设计。举个例子,有个页面比较特殊,需要扩展 Vue 原型,但是其他页面不用,如果在同一个 webpack 包裹内的话可能其他页面也被做了不需要的扩展。

所以换个思路来,如果我提供一个页面间通信方案和共享存储区域能否解决你的问题?比如我注入一个 g 对象,不管在哪个页面拿到的 g 对象都肯定是同一个。

@huadong
Copy link
Author

huadong commented Jan 9, 2020

我明白的你的诉求了,你是希望:假设在页面 A 和页面 B 都引入了 c.js 的情况下,这个 c.js 是运行时也应该是同一个,页面 A 调 c.js 存入的变量在页面 B 是应该是可以正常获取出来的。

基本是这个意思

kbone 一开始的设计初衷确实是以实现 Web 端写法为目的的,Web 端不同页面的共用模块是隔离的,所以不能偏离这个设计

这个问题最重要,这是kbone的对标定位问题。不过“写法”和支撑能力却是值得思考的问题。比如说很多“语法糖”提高了代码编写的效率,但并没有限制编译后代码的运行能力。

小程序的page和web端的page是有区别的,有时候感觉更像vue在router下的一个component,是介于component和web page之间的感觉。

webpack对于多entry打包,并没有强制要求每个entry必须是一个隔离的包裹,是允许多个入口点引入到同一个页面中的,因此有了对runtime的独立打包配置:optimization.runtimeChunk

Imported modules are initialized for each runtime chunk separately, so if you include multiple entry points on a page, beware of this behavior. You will probably want to set it to single or use another configuration that allows you to only have one runtime instance.

你提到的Vue的扩展是一个很好的需要隔离的例子。但如果我们引入axios呢,一般我们会在app.js统一配置axios的adapter、interceptors等,然后每个页面直接require axios,直接使用就好了,而无需每个page都要对axios进行一次初始化配置,大家共享一个实例就好了。当然可以把app.js初始化的axios实例通过globalData进行传递,但这个使用就不那么友好了,而且类似这种共享对象实例还不少。

这是一种机制问题,kbone框架本身使用了这种机制,然而对page的约束限制了这种机制的使用;但是使用kbone + webpack打包后,所有代码都无法逃离这个框架。也许使用自定义组件可以?

或者有没有一种方式,可以把一些代码、和指定的第三方依赖包打包放在全局使用?

所以换个思路来,如果我提供一个页面间通信方案和共享存储区域能否解决你的问题?

这里涉及到的其实是个软件工程问题,如果单纯的实现页面通信或者数据共享,直接用globalData就可以了。主要是同一个js module在页面间的唯一性机制没有了,很多第三方包的使用会有问题。无法直接使用了。像kbone那样连Event、Node等都从0造起,对大多数公司和团队来说是做不到的。

@huadong
Copy link
Author

huadong commented Jan 9, 2020

@JuneAndGreen 再举个例子,你看看如果用kbone去做一个多tab的小程序,实现类似QQ音乐APP的底部浮动播放器的功能,会有什么不便利的地方。

功能细节要求:

  1. 如果有音乐在播放,每个tab page都出现浮动播放器,没有音乐,则不出现。
  2. 下拉Android顶部菜单,或者上划iOS底部菜单,通过Android、iOS系统界面停止/播放音乐播放,要求小程序每个浮动播放器的播放状态自动从播放变成停止/播放状态。

@YikaJ
Copy link

YikaJ commented Jan 10, 2020

小程序在 kbone 跨页面使用 Event 库也有这类问题,一个页面监听的事件,另一个页面触发也不会生效的。我能想到的就是像这类需要跨页面使用的库,只能通过 getApp 挂载到小程序全局上,再来使用了。但对应到 web 的多页,又没法找到合适的办法进行兼容。

或者说官方是否有针对这类情况提供一个推荐的兼容解决方案?

@JuneAndGreen
Copy link
Collaborator

JuneAndGreen commented Jan 12, 2020

主要还是最开始的前提问题,因为 Web 端页面是隔离的,所以肯定不能破坏这个隔离的机制。你这种应该是单纯想用 web 语法来编写小程序,所以更重视小程序的设计而不是偏向于 Web 端的设计,这个得思忖一下有没有其他的思路才行,比如你的建议:把部分文件打到公共 runtime 之类。这个我记录一下。

@huadong
Copy link
Author

huadong commented Jan 12, 2020

这两天尝试修改了一下kbone的webpack插件mp-webpack-plugin,使用optimization.runtimeChunk = single进行打包,调整了一下webpack的runtime引用方式,在app.js引入runtime,各pages.js复用这个runtime,总算进一步理解kbone的window囚笼了:

function wrapChunks(compilation, chunks, globalVarsConfig) {
    chunks.forEach(chunk => {
        chunk.files.forEach(fileName => {
            if (ModuleFilenameHelpers.matchObject({test: /\.js$/}, fileName)) {
                // 页面 js
                const headerContent = 'module.exports = function(window, document) {const App = function(options) {window.appOptions = options};' + globalVars.map(item => `var ${item} = window.${item}`).join(';') + ';'
                let customHeaderContent = globalVarsConfig.map(item => `var ${item[0]} = ${item[1] ? item[1] : 'window[\'' + item[0] + '\']'}`).join(';')
                customHeaderContent = customHeaderContent ? customHeaderContent + ';' : ''
                const footerContent = '}'

                compilation.assets[fileName] = new ConcatSource(headerContent + customHeaderContent, compilation.assets[fileName], footerContent)
            }
        })
    })
}

这里的:

function(window, document) {}

由webpack bundle + window 组成的囚笼确实很牢固的,虽然可能可以通过webpack的optimization.splitChunks进行深度的打包配置,再加上使用mp-webpack-plugin进行bundle拆离引入,但可能还是很晦涩。这让我想起了GitHub CTO Jason Warner说的: 如果你发现自己受制于你所写的技术,那么在接下来的 18 个月里,你将会陷入痛苦的漩涡中。

这意味着在kbone当前的设计下,很难实现全局的状态管理机制,类似redux、vuex、mobix之类的。

  • kbone可以用来做H5网站迁移?但其实H5和小程序各有千秋,一个完整的H5网站到小程序的迁移本身就是个问题,用kbone也是无济于事。
  • kbone可以同时快速开发H5站点+小程序,一套代码?对小程序而言,这样做出来的小程序很不“友好”。
  • kbone可以用来。。。

算来算去,kbone有点“鸡肋”了,感觉有点可惜。

其实我希望的不是单纯用web的语法来写小程序,而是希望能在写小程序的时候可以引用海量的、成熟的、可复用的项目代码。

@JuneAndGreen
Copy link
Collaborator

JuneAndGreen commented Jan 13, 2020

这样理解不对,kbone 本身是作为适配器一般的存在,来支持原本的 Web 端代码在小程序运行,Web 端代码是按页面粒度分离,所以到小程序端亦是如此实现。

但是想两个平台完全互通兼容目前来说确实是做不到的。事实上在 Web 端两个页面的全局状态也是不互通的,除非是 spa 中的单页切换,页面不刷新就可以保留全局状态。这也就是设计问题,小程序的页面对标了多页 Web 应用的页面,而不是 spa 中的单页。

如果想将 spa 直接对标一个小程序,这是另一种不错的设计思路,但是孰优孰劣其实没法直接衡量,也许后面可以尝试给出另一种插件来实现这个设计。

@YikaJ
Copy link

YikaJ commented Jan 13, 2020

这样理解不对,kbone 本身是作为适配器一般的存在,来支持原本的 Web 端代码在小程序运行,Web 端代码是按页面粒度分离,所以到小程序端亦是如此实现。

但是想两个平台完全互通兼容目前来说确实是做不到的。事实上在 Web 端两个页面的全局状态也是不互通的,除非是 spa 中的单页切换,页面不刷新就可以保留全局状态。这也就是设计问题,小程序的页面对标了多页 Web 应用的页面,而不是 spa 中的单页。

如果想将 spa 直接对标一个小程序,这是另一种不错的设计思路,但是孰优孰劣其实没法直接衡量,也许后面可以尝试给出另一种插件来实现这个设计。

我们就是正在尝试采用该方法,小程序的多页对应 Web 的 SPA,这样可以让两个平台都有比较正常的开发和使用体验。 @Ryqsky

@huadong
Copy link
Author

huadong commented Jan 13, 2020

这样理解不对,kbone 本身是作为适配器一般的存在,来支持原本的 Web 端代码在小程序运行,Web 端代码是按页面粒度分离,所以到小程序端亦是如此实现。

让10多亿人内置kbone的核心组件,同时在小程序社区进行了大力的宣扬,而且围绕kbone还做了很多的支持性开发,难免对kbone有所期许。因为对于已存在的Web端代码,其后端一般是连着公众号的业务代码,按目前微信的技术生态体系,它的迁移还是需要颇费一番功夫。与其迁移,不如重写一个小程序版的;而且好多微站都是技术服务商提供的开发。

如果想将 spa 直接对标一个小程序,这是另一种不错的设计思路,但是孰优孰劣其实没法直接衡量,也许后面可以尝试给出另一种插件来实现这个设计。

反过来,按小程序目前的runtime设计,更适合对标的就是SPA。我觉得今天微信小程序所获得的发展,和小程序类似SPA的开发技术、同时在用户操作上更贴近APP的习惯是分不开的。相当于复用了熟悉SPA技术的程序员,解决了H5微站不那么友好的用户操作问题。

Kbone的整体设计思路、代码行文可圈可点,通过简单的webpack打包配置和mp-webpack-plugin的少量修改,可以实现webpack的runtime按全局配置还是按照pages独立配置。当前通过createPage()封装调用,使得page+window+document是捆绑的;如果修改kbone的miniprogram-render(当然这么一改就失去了微信小程序的内置😭),是不是可以实现SPA的对标?拿Vue举例,小程序的多pages相当于SPA里面的多Vue实例(多root)。

对前端技术没太多的研究,应该还有更好的实现方案。还是那句话:“kbone当前的支持能力,有点可惜了。”期待它的进一步发展!

@JuneAndGreen
Copy link
Collaborator

跨页 cookie 已支持,即所有页面 cookie 共享一个存储。已发布在 [email protected] & [email protected] 上,相关文档:https://wechat-miniprogram.github.io/kbone/docs/config/#runtime-cookiestore

@huadong
Copy link
Author

huadong commented Feb 15, 2020

cookie 这么parse会有bug的:

        // key-value
        const parseKeyValue = /^([^=;\x00-\x1F]+)=([^;\n\r\0\x00-\x1F]*).*/.exec(cookieStr.shift())
        if (!parseKeyValue) return null

        const key = (parseKeyValue[1] || '').trim()
        const value = (parseKeyValue[2] || '').trim()

我提交一个PR吧 #81

@JuneAndGreen
Copy link
Collaborator

此 pr 有问题哈,cookie 如此解析主要是要对齐 document.cookie 的 setter 方法。

@huadong
Copy link
Author

huadong commented Feb 19, 2020

腾讯小程序的不同版本API也会有Bug。出现过第一下不是key=value的版本。

@JuneAndGreen
Copy link
Collaborator

如果是微信小程序/公众平台/开放平台这边提供的接口出现这样的问题,可以到 https://developers.weixin.qq.com/ 社区这边提问哈,把规范甩上去。如果是其他应用提供的接口,估计得去相应的反馈渠道去反馈了。

@xmsz
Copy link

xmsz commented Feb 24, 2020

我的看法

确实对于大多数人来说,小程序对标的是Web的单页

市面上的框架也好,插件也好都是把改小程序改造的更像单页应用。这个是大家默认的方向和事情。
所以之前第一次用 kbone 的时候,是有点满头雾水的。因为我不知道如果把小程序做成多页的优势是什么? 是类似与一些其他同构方案,实现在原有框架中,嵌入一个别的框架写的页面?

变成多页的好处没有,倒是局限变得多了。这一点更难理解,明明大部分情况下使用单页就是为了优化多页的体验,现在又变回去了,只为了做到规范的「隔离」?

所以我也很矛盾,因为一方面如果只站在技术的角度,我觉得kbone 的做法合情合理,也本来就是这么做。
但是从使用上、目的上又希望把小程序做出单页,这样更贴近需求

@huadong
Copy link
Author

huadong commented Feb 27, 2020

对标问题:

  • 小程序 -> APP:单页
  • kbone -> browser:多页

应用场景不太一样。目前大家都是对标着APP开发小程序的。

@JuneAndGreen
Copy link
Collaborator

@hanjunspirit
Copy link

@JuneAndGreen demo22中的共享vuex state的方式存在一定的问题:

一个是内存泄漏问题:
多个页面的store使用的是同一个state对象,每个页面都把state对象定义成响应式的(也就是给每个属性定义getter,setter,第二个页面的getter,setter会在前一个的基础上再套一层),当页面被销毁时,这些getter,setter还在。
当你把一个对象赋值到state的某个属性时会触setter,vue会把这个对象定义成响应式的,问题在于这些被销毁的页面的setter仍然在定义自己的响应式。显然这些响应式永远用不着,这里会有一定的内存泄漏

一个是响应式对象的__ob__属性相互覆盖问题:
vue会为响应式对象添加__ob__属性,来表示这个对象已经是响应式的。多个页面的__ob__属性会相互覆盖,这导致某些依赖这个__ob__属性的场景会出现非预期的结果,比如以下两点需要注意

比如把一个已经被定义成响应式的对象赋值到state的某个属性时,因为vue内部是通过instanceof Observer来判断的
image
多个页面的Observer不是同一个对象。这会导致vue认为这对象不是响应式的,会重复定义响应式。如果频繁设置会导致严重的内存泄露。

比如调用数组的push,vue通过改写数组push,unshift,splice方法来实现数组这类变化的监听,只有__ob__属性所属的页面才能收到变化的通知。
image

@JuneAndGreen
Copy link
Collaborator

@hanjunspirit 明白,确实可能有这个问题,先前写这个 demo 考虑不够周详。我看看能否提供一个工具方法来处理这个问题。

@JuneAndGreen
Copy link
Collaborator

@JuneAndGreen demo22中的共享vuex state的方式存在一定的问题:

一个是内存泄漏问题:
多个页面的store使用的是同一个state对象,每个页面都把state对象定义成响应式的(也就是给每个属性定义getter,setter,第二个页面的getter,setter会在前一个的基础上再套一层),当页面被销毁时,这些getter,setter还在。
当你把一个对象赋值到state的某个属性时会触setter,vue会把这个对象定义成响应式的,问题在于这些被销毁的页面的setter仍然在定义自己的响应式。显然这些响应式永远用不着,这里会有一定的内存泄漏

一个是响应式对象的__ob__属性相互覆盖问题:
vue会为响应式对象添加__ob__属性,来表示这个对象已经是响应式的。多个页面的__ob__属性会相互覆盖,这导致某些依赖这个__ob__属性的场景会出现非预期的结果,比如以下两点需要注意

比如把一个已经被定义成响应式的对象赋值到state的某个属性时,因为vue内部是通过instanceof Observer来判断的
image
多个页面的Observer不是同一个对象。这会导致vue认为这对象不是响应式的,会重复定义响应式。如果频繁设置会导致严重的内存泄露。

比如调用数组的push,vue通过改写数组push,unshift,splice方法来实现数组这类变化的监听,只有__ob__属性所属的页面才能收到变化的通知。
image

内存泄漏问题,目前暂时没有其他更优雅的方法,还是比较建议每次页面进入时刷新 state 的方式来干掉其他页面加入的 getter 和 setter。具体可参考 demo22。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants