后续维护迁移至CS-Interview-Knowledge-Map
- 开言
- 小程序架构
- View - WXML
- View - WXSS
- View - Component
- View - Native Component
- 目前小程序的问题或限制
- 授权获取用户信息流程
- 性能优化
- 官方小程序技术能力规划
- wepy vs mpvue
- mpvue
- 参考链接
开发中使用库当然没问题,但前提是不要仅仅只知道怎么使用,更要知道它的工作原理。如果不能超越这些库,那在这些库变成你的拐杖之时,你也会随之“残废”。
知其然,并知其所以然。时刻要有一颗追本溯源的心。
微信小程序的框架包含两部分View视图层、App Service逻辑层。View层用来渲染页面结构,AppService层用来逻辑处理、数据请求、接口调用。
它们在两个线程里运行。
它们在两个线程里运行。
它们在两个线程里运行。
视图层和逻辑层通过系统层的JSBridage进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
补充
视图层使用WebView渲染,iOS中使用自带WKWebView,在android使用腾讯的x5内核(基于Blink)运行。
逻辑层使用在iOS中使用自带的JSCore运行,在android中使用腾讯的x5内核(基于Blink)运行。
开发工具使用nw.js同时提供了视图层和逻辑层的运行环境。
在mac下使用js-beautify对微信开发工具@v1.02.1808080代码批量格式化:
cd /Applications/wechatwebdevtools.app/Contents/Resources/package.nw
find . -type f -name '*.js' -not -path "./node_modules/*" -not -path -exec js-beautify -r -s 2 -p -f '{}' \;
在js/extensions/appservice/index.js中找到:
267: function(a, b, c) {
const d = c(8),
e = c(227),
f = c(226),
g = c(228),
h = c(229),
i = c(230);
var j = window.__global.navigator.userAgent,
k = -1 !== j.indexOf('game');
k || i(), window.__global.getNewWeixinJSBridge = (a) => {
const {
invoke: b
} = f(a), {
publish: c
} = g(a), {
subscribe: d,
triggerSubscribeEvent: i
} = h(a), {
on: j,
triggerOnEvent: k
} = e(a);
return {
invoke: b,
publish: c,
subscribe: d,
on: j,
get __triggerOnEvent() {
return k
},
get __triggerSubscribeEvent() {
return i
}
}
}, window.WeixinJSBridge = window.__global.WeixinJSBridge = window.__global.getNewWeixinJSBridge('global'), window.__global.WeixinJSBridgeMap = {
__globalBridge: window.WeixinJSBridge
}, __devtoolsConfig.online && __devtoolsConfig.autoTest && setInterval(() => {
console.clear()
}, 1e4);
try {
var l = new window.__global.XMLHttpRequest;
l.responseType = 'text', l.open('GET', `http://${window.location.host}/calibration/${Date.now()}`, !0), l.send()
} catch (a) {}
}
在js/extensions/gamenaitveview/index.js中找到:
299: function(a, b, c) {
'use strict';
Object.defineProperty(b, '__esModule', {
value: !0
});
var d = c(242),
e = c(241),
f = c(243),
g = c(244);
window.WeixinJSBridge = {
on: d.a,
invoke: e.a,
publish: f.a,
subscribe: g.a
}
},
在js/extensions/pageframe/index.js中找到:
317: function(a, b, c) {
'use strict';
function d() {
window.WeixinJSBridge = {
on: e.a,
invoke: f.a,
publish: g.a,
subscribe: h.a
}, k.a.init();
let a = document.createEvent('UIEvent');
a.initEvent('WeixinJSBridgeReady', !1, !1), document.dispatchEvent(a), i.a.init()
}
Object.defineProperty(b, '__esModule', {
value: !0
});
var e = c(254),
f = c(253),
g = c(255),
h = c(256),
i = c(86),
j = c(257),
k = c.n(j);
'complete' === document.readyState ? d() : window.addEventListener('load', function() {
d()
})
},
我们都看到了WeixinJSBridge的定义。分别都有on
,invoke
,publish
,subscribe
这个几个关键方法。
拿invoke
举例,在js/extensions/appservice/index.js中发现这段代码:
f (!r) p[b] = s, f.send({
command: 'APPSERVICE_INVOKE',
data: {
api: c,
args: e,
callbackID: b
}
});
在js/extensions/pageframe/index.js中发现这段代码:
g[d] = c, e.a.send({
command: 'WEBVIEW_INVOKE',
data: {
api: a,
args: b,
callbackID: d
}
})
简单的分析得知:字段command用来区分行为,invoke用来调用native的api。在不同的来源要使用不同的前缀。data里面包含api名,参数。另外callbackID指定接受回调的方法句柄。appservice和webview使用的通信协议是一致的。
我们不能在代码里使用BOM和DOM是因为根本没有,另一方面也不希望JS代码直接操作视图。
在开发工具中remote-helper.js中找到了这样的代码:
const vm = require("vm");
const vmGlobal = {
require: undefined,
eval: undefined,
process: undefined,
setTimeout(...args) {
//...省略代码
return timerCount;
},
clearTimeout(id) {
const timer = timers[id];
if (timer) {
clearTimeout(timer);
delete timers[id];
}
},
setInterval(...args) {
//...省略代码
return timerCount;
},
clearInterval(id) {
const timer = timers[id];
if (timer) {
clearInterval(timer);
delete timers[id];
}
},
console: (() => {
//...省略代码
return consoleClone;
})()
};
const jsVm = vm.createContext(vmGlobal);
// 省略大量代码...
function loadCode(filePath, sourceURL, content) {
let ret;
try {
const script = typeof content === 'string' ? content : fs.readFileSync(filePath, 'utf-8').toString();
ret = vm.runInContext(script, jsVm, {
filename: sourceURL,
});
}
catch (e) {
// something went wrong in user code
console.error(e);
}
return ret;
}
这样的分层设计显然是有意为之的,它的中间层完全控制了程序对于界面进行的操作, 同时对于传递的数据和响应时间也能做到监控。一方面程序的行为受到了极大限制, 另一方面微信可以确保他们对于小程序内容和体验有绝对的控制。
这样的结构也说明了小程序的动画和绘图 API 被设计成生成一个最终对象而不是一步一步执行的样子, 原因就是 json格式的数据传递和解析相比与原生 API 都是损耗不菲的,如果频繁调用很可能损耗过多性能,进而影响用户体验。
1.动画需要绑定在data上,而绘图却不用。你觉得是为什么呢?
var context = wx.createCanvasContext('firstCanvas')
context.setStrokeStyle("#00ff00")
context.setLineWidth(5)
context.rect(0, 0, 200, 200)
context.stroke()
context.setStrokeStyle("#ff0000")
context.setLineWidth(2)
context.moveTo(160, 100)
context.arc(100, 100, 60, 0, 2 * Math.PI, true)
context.moveTo(140, 100)
context.arc(100, 100, 40, 0, Math.PI, false)
context.moveTo(85, 80)
context.arc(80, 80, 5, 0, 2 * Math.PI, true)
context.moveTo(125, 80)
context.arc(120, 80, 5, 0, 2 * Math.PI, true)
context.stroke()
context.draw()
Page({
data: {
animationData: {}
},
onShow: function(){
var animation = wx.createAnimation({
duration: 1000,
timingFunction: 'ease',
})
this.animation = animation
animation.scale(2,2).rotate(45).step()
this.setData({
animationData:animation.export()
})
}
})
2.小程序的http rquest请求是不是用的浏览器fetch API?
知识点考察
- 知道request是由native实现的
- jscore是不带http request、websocket、storage等功能的,那是webkit带的
- 小程序的wx.request是不是遵循fetch API规范实现的呢?答案,显然不是。因为没有Promise。
WXML(WeiXin Markup Language)
- 支持数据绑定
- 支持逻辑算术、运算
- 支持模板、引用
- 支持添加事件(bindtap)
wxml编译器:wcc 把wxml文件 转为 js
执行方式:wcc index.wxml
使用Virtual DOM,进行局部更新
WXSS(WeiXin Style Sheets)
wxss编译器:wcsc 把wxss文件转化为 js
执行方式: wcsc index.wxss
亲测包含但不限于如下内容:
- Transition
- Animation
- Keyframes
- border-radius
- calc()
- 选择器,除了官方文档列出的,其实还支持
- element>element
- element+element
- element element
- element:first-letter
- element:first-line
- element:first-child
- element:last-child
- element~element
- element:first-of-type
- element:last-of-type
- element:only-of-type
- element:only-child
- element:nth-child(n)
- element:nth-last-child(n)
- element:nth-of-type(n)
- element:nth-last-of-type(n)
- :root
- element:empty
- :not(element)
- iconfont
建议css3的特性都可以做一下尝试。
rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。公式:
const dsWidth = 750
export const screenHeightOfRpx = function () {
return 750 / env.screenWidth * env.screenHeight
}
export const rpxToPx = function (rpx) {
return env.screenWidth / 750 * rpx
}
export const pxToRpx = function (px) {
return 750 / env.screenWidth * px
}
设备 | rpx换算px (屏幕宽度/750) | px换算rpx (750/屏幕宽度) |
---|---|---|
iPhone5 | 1rpx = 0.42px | 1px = 2.34rpx |
iPhone6 | 1rpx = 0.5px | 1px = 2rpx |
iPhone6 Plus | 1rpx = 0.552px | 1px = 1.81rpx |
可以了解一下pr2rpx-loader这个库。
使用@import
语句可以导入外联样式表,@import
后跟需要导入的外联样式表的相对路径,用;
表示语句结束。
静态的样式统一写到 class 中。style 接收动态的样式,在运行时会进行解析,请尽量避免将静态的样式写进 style 中,以免影响渲染速度。
定义在 app.wxss 中的样式为全局样式,作用于每一个页面。在 page 的 wxss 文件中定义的样式为局部样式,只作用在对应的页面,并会覆盖 app.wxss 中相同的选择器。
截止20180810
小程序未来有计划支持字体。参考微信公开课。
小程序开发与平时web开发类似,也可以使用字体图标,但是src:url()无论本地还是远程地址都不行,base64值则都是可以显示的。
将ttf文件转换成base64。打开这个平台transfonter.org/。点击Add fonts按钮,加载ttf格式的那个文件。将下边的base64 encode改为on。点击Convert按钮进行转换。转换后点击download下载。
复制下载的压缩文件中的stylesheet.css的内容到font.wxss,并且将icomoon中的style.css除了@font-face所有的代码也复制到font.wxss并将i选择器换成.iconfont。最后:
<text class="iconfont icon-home" style="font-size:50px;color:red"></text>
小程序提供了一系列组件用于开发业务功能,按照功能与HTML5的标签进行对比如下:
小程序的组件基于Web Component标准
使用Polymer框架实现Web Component
目前Native实现的组件有
Native组件层在WebView层之上。这目前带来了一些问题:
- 1.Native实现的组件会遮挡其他组件
- 2.WebView渲染出来的视图在滚动时,Native实现的组件需要更新位置,这会带来性能问题,在安卓机器上比较明显
- 3.小程序原生组件cover-view可以覆盖 cavnas video等,但是也有一下弊端,比如在cavnas上覆盖cover-view,就会发现坐标系不统一处理麻烦
截止20180810
包含但不限于:
-
小程序仍然使用WebView渲染,并非原生渲染。(部分原生)
-
服务端接口返回的头无法执行,比如:Set-Cookie。
-
依赖浏览器环境的js库不能使用。
-
不能使用npm,但是可以自搭构建工具或者使用mpvue。(未来官方有计划支持)
-
不能使用ES7,可以自己用babel+webpack自搭或者使用mpvue。
-
不支持使用自己的字体(未来官方计划支持)。
-
可以用base64的方式来使用iconfont。
-
小程序不能发朋友圈(可以通过保存图片到本地,发图片到朋友前。二维码可以使用B接口)。
-
获取二维码/小程序接口的限制。
- B接口scene最大32个可见字符。
- AC接口总共生成的码数量限制为100,000,请谨慎调用。
- 真机扫描二维码只能跳转到线上版本,所以测试环境下只可通过开发者工具的‘通过二维码编译’进行调试。
- 没有发布到线上版本的小程序页面路径会导致生成二维码失败,需要先将添加了页面的小程序发布到线上版本。
-
小程序推送只能使用“服务通知” 而且需要用户主动触发提交formId,formId只有7天有效期。(现在的做法是在每个页面都放入form并且隐藏以此获取更多的formId。后端使用原则为:优先使用有效期最短的)
-
小程序大小限制2M,分包总计不超过8M
-
转发(分享)小程序不能拿到成功结果,原来可以。链接(小游戏造的孽)
-
拿到相同的unionId必须绑在同一个开放平台下。开放平台绑定限制:
- 50个移动应用
- 10个网站
- 50个同主体公众号
- 5个不同主体公众号
- 50个同主体小程序
- 5个不同主体小程序
-
公众号关联小程序,链接
- 所有公众号都可以关联小程序。
- 一个公众号可关联10个同主体的小程序,3个不同主体的小程序。
- 一个小程序可关联500个公众号。
- 公众号一个月可新增关联小程序13次,小程序一个月可新增关联500次。
-
一个公众号关联的10个同主体小程序和3个非同主体小程序可以互相跳转
-
品牌搜索不支持金融、医疗
-
小程序授权需要用户主动点击
-
小程序不提供测试access_token
-
安卓系统下,小程序授权获取用户信息之后,删除小程序再重新获取,并重新授权,得到旧签名,导致第一次授权失败
-
开发者工具上,授权获取用户信息之后,如果清缓存选择全部清除,则即使使用了wx.checkSession,并且在session_key有效期内,授权获取用户信息也会得到新的session_key
为了验证小程序对HTTP的支持适配情况,我找了两个服务器做测试,一个是网上搜索到支持HTTP2的服务器,一个是我本地起的一个HTTP2服务器。测试中所有请求方法均使用wx.request
。
-
网上支持HTTP2的服务器:
HTTPs://www.snel.com:443
-
在Chrome上查看该服务器为HTTP2
-
在模拟器上请求该接口,
请求头
的HTTP版本为HTTP1.1,模拟器不支持HTTP2 -
由于小程序线上环境需要在项目管理里配置请求域名,而这个域名不是我们需要的请求域名,没必要浪费一个域名位置,所以打开不验证域名,TSL等选项请求该接口,通过抓包工具表现与模拟器相同
由上可以看出,在真机与模拟器都不支持HTTP2,但是都是成功请求的,并且响应头
里的HTTP版本都变成了HTTP1.1版本,说明服务器对HTTP1.1做了兼容性适配。
-
本地新启一个node服务器,返回json为请求的HTTP版本
-
如果服务器只支持HTTP2,在模拟器请求时发生了一个
ALPN
协议的错误。并且提醒使用适配HTTP1。 -
当把服务器的
allowHTTP1
,设置为true
,并在请求时处理相关相关请求参数后,模拟器能正常访问接口,并打印出对应的HTTP请求版本
- session_key有有效期,有效期并没有被告知开发者,只知道用户越频繁使用小程序,session_key有效期越长
- 在调用wx.login时会直接更新session_key,导致旧session_key失效
- 小程序内先调用wx.checkSession检查登录态,并保证没有过期的session_key不会被更新,再调用wx.login获取code。接着用户授权小程序获取用户信息,小程序拿到加密后的用户数据,把加密数据和code传给后端服务。后端通过code拿到session_key并解密数据,将解密后的用户信息返回给小程序
面试题:先授权获取用户信息再login会发生什么?
- 用户授权时,开放平台使用旧的session_key对用户信息进行加密。调用wx.login重新登录,会刷新session_key,这时后端服务从开放平台获取到新session_key,但是无法对老session_key加密过的数据解密,用户信息获取失败
- 在用户信息授权之前先调用wx.checkSession呢?wx.checkSession检查登录态,并且保证wx.login不会刷新session_key,从而让后端服务正确解密数据。但是这里存在一个问题,如果小程序较长时间不用导致session_key过期,则wx.login必定会重新生成session_key,从而再一次导致用户信息解密失败。
我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。
我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。
我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。
代码包的大小是最直接影响小程序加载启动速度的因素。代码包越大不仅下载速度时间长,业务代码注入时间也会变长。所以最好的优化方式就是减少代码包的大小。
小程序加载的三个阶段的表示。
优化方式
- 代码压缩。
- 及时清理无用代码和资源文件。
- 减少代码包中的图片等资源文件的大小和数量。
- 分包加载。
首屏加载的体验优化建议
- 提前请求: 异步数据请求不需要等待页面渲染完成。
- 利用缓存: 利用storage API对异步请求数据进行缓存,二次启动时先利用缓存数据渲染页面,在进行后台更新。
- 避免白屏:先展示页面骨架页和基础内容。
- 及时反馈:即时地对需要用户等待的交互操作给出反馈,避免用户以为小程序无响应。
在构建小程序分包项目时,构建会输出一个或多个功能的分包,其中每个分包小程序必定含有一个主包,所谓的主包,即放置默认启动页面/TabBar 页面,以及一些所有分包都需用到公共资源/JS 脚本,而分包则是根据开发者的配置进行划分。
在小程序启动时,默认会下载主包并启动主包内页面,如果用户需要打开分包内某个页面,客户端会把对应分包下载下来,下载完成后再进行展示。
优点:
- 对开发者而言,能使小程序有更大的代码体积,承载更多的功能与服务
- 对用户而言,可以更快地打开小程序,同时在不影响启动速度前提下使用更多功能
限制:
- 整个小程序所有分包大小不超过 8M
- 单个分包/主包大小不能超过 2M
原生分包加载的配置 假设支持分包的小程序目录结构如下:
├── app.js
├── app.json
├── app.wxss
├── packageA
│ └── pages
│ ├── cat
│ └── dog
├── packageB
│ └── pages
│ ├── apple
│ └── banana
├── pages
│ ├── index
│ └── logs
└── utils
开发者通过在 app.json subPackages 字段声明项目分包结构:
{
"pages":[
"pages/index",
"pages/logs"
],
"subPackages": [
{
"root": "packageA",
"pages": [
"pages/cat",
"pages/dog"
]
}, {
"root": "packageB",
"pages": [
"pages/apple",
"pages/banana"
]
}
]
}
分包原则
- 声明 subPackages 后,将按 subPackages 配置路径进行打包,subPackages 配置路径外的目录将被打包到 app(主包) 中
- app(主包)也可以有自己的 pages(即最外层的 pages 字段
- subPackage 的根目录不能是另外一个 subPackage 内的子目录
- 首页的 TAB 页面必须在 app(主包)内
引用原则
- packageA 无法 require packageB JS 文件,但可以 require app、自己 package 内的 JS 文件
- packageA 无法 import packageB 的 template,但可以 require app、自己 package 内的 template
- packageA 无法使用 packageB 的资源,但可以使用 app、自己 package 内的资源
官方即将推出 分包预加载
独立分包
-
每次setData的调用都是一次进程间通信过程,通信开销与setData的数据量正相关。
-
setData会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互。
-
setData是小程序开发使用最频繁,也是最容易引发性能问题的。
避免不当使用setData
- 使用data在方法间共享数据,可能增加setData传输的数据量。。data应仅包括与页面渲染相关的数据。
- 使用setData传输大量数据,**通讯耗时与数据正相关,页面更新延迟可能造成页面更新开销增加。**仅传输页面中发生变化的数据,使用setData的特殊key实现局部更新。
- 短时间内频繁调用setData,**操作卡顿,交互延迟,阻塞通信,页面渲染延迟。**避免不必要的setData,对连续的setData调用进行合并。
- 在后台页面进行setData,**抢占前台页面的渲染资源。**页面切入后台后的setData调用,延迟到页面重新展示时执行。
避免不当使用onPageScroll
- 只在有必要的时候监听pageScroll事件。不监听,则不会派发。
- 避免在onPageScroll中执行复杂逻辑
- 避免在onPageScroll中频繁调用setData
- 避免滑动时频繁查询节点信息(SelectQuery)用以判断是否显示,部分场景建议使用节点布局橡胶状态监听(inersectionObserver)替代
使用自定义组件
在需要频繁更新的场景下,自定义组件的更新只在组件内部进行,不受页面其他部分内容复杂性影响。
小程序的几个页面间,存在一些相同或是类似的区域,这时候可以把这些区域逻辑封装成一个自定义组件,代码就可以重用,或者对于比较独立逻辑,也可以把它封装成一个自定义组件,也就是微信去年发布的自定义组件,它让代码得到复用、减少代码量,更方便模块化,优化代码架构组织,也使得模块清晰,后期更好地维护,从而保证更好的性能。
但微信打算在原来的基础上推出的自定义组件2.0,它将拥有更高级的性能:
- usingComponents计划支持全局定义和通配符定义:这意味着不用在每个页面反复定义,可以批量导入目录下的所有自定义组件
- 计划支持类似Computed和watch的功能,它能使代码逻辑更清晰
- 计划支持Component构造器插件,在实例化一个自定义组件的时候,允许你在构造器的这个阶段,加入一些逻辑,方便进行一些扩展,甚至是可以扩展成VUE的语法
目前小程序开发的痛点是:开源组件要手动复制到项目,后续更新组件也需要手动操作。不久的将来,小程序将支持npm包管理,有了这个之后,想要引入一些开源的项目就变得很简单了,只要在项目里面声明,然后用简单的命令安装,就可以使用了。
微信小程序团队表示,他们在考虑推出一些官方自定义组件,为什么不内置到基础库里呢?因为内置组件要提供给开发者,这个组件一定是开发者很难实现或者是无法实现的一个能力。所以他们更倾向于封装成自定义组件,想基于这些内置组件里,封装一些比较常见的、交互逻辑比较复杂的组件给大家使用,让大家更容易开发。类似弹幕组件,开发者就不用关注弹幕怎么飘,可以节省开发者的开发成本。
同时,他们也想给开发者提供一些规范和一些模板,让开发者设计出好用的自定义组件,更好地被大家去使用。
当小程序加载太慢时,可能会导致用户的流失,而小程序的开发者可能会面临着不知道如何定位问题或不知如何解决问题的困境。
为此,小程序即将推出一个体验评分的功能,这是为了帮助开发者可以检查出小程序有一些什么体验不好的地方,也会同时给出一份优化的指引建议。
小程序在最初的技术选型时,引入了原生组件的概念,因为原生组件可以使小程序的能力更加丰富,比如地图、音视频的能力,但是原生组件是由客户端原生渲染的,导致了原生组件的层级是最高的,开发者很容易遇到打开调试的问题,发现视频组件挡在了vConsole上。
为了解决这个问题,当时微信做了一个过渡的方案:cover-view。cover-view可以覆盖在原生组件之上,这一套方案解决了大部分的需求场景。比如说视频组件上很多的按钮、标题甚至还有动画的弹幕,这些都是用cover-view去实现的,但它还是没有完全解决原生组件的开发体验问题,因为cover-view有一些限制:
- 无法与其他组件混在一起渲染
- 没有完整的触摸事件
- cover-view对样式的表现有差异
- cover-view对样式的支持度不够高
因此微信决定将用同层渲染取代cover-view,它能像普通组件一样使用,原生组件的层级不再是最高,而是和其他的非原生组件在同一层级渲染,可完全由z-index控制,可完全支持触摸事件。
微信表示,同层渲染在iOS平台小程序上已经开始内测,会很快开放给开发者,Android平台已经取得突破性进展,目前正在做一轮封装的工作,开放指日可待。
相比传统的小程序框架,这个一直是我们作为资深开发者比较期望去解决的,在web开发中,随着 flux、redux、vuex 等多个数据流工具出现,我们也期望在业务复杂的小程序中使用。
-
WePY 默认支持 Redux,在脚手架生成项目的时候可以内置
-
mpvue 作为 vue 的移植版本,当然支持 vuex,同样在脚手架生成项目的时候可以内置
如果你和我们一样,经历了从无到有的小程序业务开发,建议阅读【小程序的组件化开发】章节,进行官方语法的组件库开发(从基础库 1.6.3 开始,官方提供了组件化解决方案)。
- WePY 类似 vue 实现了单文件组件,最大的差别是文件后缀 .wpy,只是写法上会有差异,具体可以查看【主流框架使用案例 1:WePY】章节,学习起来有一定成本,不过也会很快适应:
export default class Index extends wepy.page {}
- mpvue 作为 vue 的移植版本,支持单文件组件,template、script 和 style 都在一个 .vue 文件中,和 vue 的写法类似,所以对 vue 开发熟悉的同学会比较适应。
所有的小程序开发依赖官方提供的开发者工具。开发者工具简单直观,对调试小程序很有帮助,现在也支持腾讯云(目前我们还没有使用,但是对新的一些开发者还是有帮助的),可以申请测试报告查看小程序在真实的移动设备上运行性能和运行效果,但是它本身没有类似前端工程化中的概念和工具。
- wepy 内置了构建,通过 wepy init 命令初始化项目,大致流程如下:
- wepy-cli 会判断模版是在远程仓库还是在本地,如果在本地则会立即跳到第 3 步,反之继续进行。
- 会从远程仓库下载模版,并保存到本地。
- 询问开发者 Project name 等问题,依据开发者的回答,创建项目。
- mpvue 沿用了 vue 中推崇的 webpack 作为构建工具,但同时提供了一些自己的插件以及配置文件的一些修改,比如:
- 不再需要 html-webpack-plugin
- 基于 webpack-dev-middleware 修改成 webpack-dev-middleware-hard-disk
- 最大的变化是基于 webpack-loader 修改成 mpvue-loader
- 但是配置方式还是类似,分环境配置文件,最终都会编译成小程序支持的目录结构和文件后缀。
对比\框架 | 微信小程序 | mpvue | wepy |
---|---|---|---|
语法规范 | 小程序开发规范 | vue.js | 类vue.js |
标签集合 | 小程序 | htm l + 小程序 | 小程序 |
样式规范 | wxss | sass,less,postcss | sass,less,styus |
组件化 | 无组件化机制 | vue规范 | 自定义组件规范 |
多段复用 | 不可复用 | 支持h5 | 支持h5 |
自动构建 | 无自动构建 | webpack | 框架内置 |
上手成本 | 全新学习 | vue 学习 | vue 和 wepy |
数据管理 | 不支持 | vuex | redux |
先说结论:选择mpvue。
wepy vs mpvue。
理由:
工程化 原生开发因为不带工程化,诸如NPM包(未来会引入)、ES7、图片压缩、PostCss、pug、ESLint等等不能用。如果自己要搭工程化,不如直接使用wepy或mpvue。mpvue和wepy都可以和小程序原生开发混写。参考mpvue-echart,参考wepy。 而问题在于wepy没有引入webpack(wepy@2.0.x依然没有引入),以上说的这些东西都要造轮子(作者造或自己造)。没有引入webpack是一个重大的硬伤。社区维护的成熟webpack显然更稳定,轮子更多。
维护 wepy也是社区维护的,是官方的?其实wepy的主要开发者只有作者一人,附上一个contrubutors链接。另外被官方招安了也是后来的事,再说腾讯要有精力帮着一起维护好wepy,为什么不花精力在小程序原生开发上呢?再来看看mpvue,是美团一个前端小组维护的。
学习成本 vue的学习曲线比较平缓。mpvue是vue的子集。所以mpvue的学习成本会低于wepy。尤其是之前技术栈有学过用过vue的。
未来规划 mpvue已经支持web和小程序。因为mpvue基于AST,所以未来可以支持支付宝小程序和快应用。他们也是有这样的规划。
坑 两者都有各自的坑。但是我觉得有一些wepy的坑是没法容忍的。比如repeat组建里面用computed得到的列表全是同一套数据而且1.x是没法解决的。 wepy和mpvue我都开发过完整小程序的体验下,我觉得wepy的坑更多,而且wepy有些坑碍于架构设计没办法解决。
Vue.js 小程序版, fork 自 vuejs/vue@2.4.1,保留了 vue runtime 能力,添加了小程序平台的支持。
mpvue
是一个使用Vue.js
开发小程序的前端框架。框架基于Vue.js
核心,mpvue
修改了Vue.js
的 runtime 和 compiler 实现,使其可以运行在小程序环境中,从而为小程序开发引入了整套Vue.js
开发体验。
两个大方向
- 通过
mpvue
提供mp的runtime适配小程序 - 通过
mpvue-loader
产出微信小程序所需要的文件结构和模块内容。
七个具体问题
要了解mpvue原理必然要了解vue原理,这是大前提。但是要讲清楚vue原理需要花费大量的篇幅,不如参考learnVue。
现在假设您对vue原理有个大概的了解。
由于Vue使用了Virtual DOM,所以Virtual DOM可以在任何支持JavaScript语言的平台上操作,譬如说目前vue支持浏览器平台或weex,也可以是mp(小程序)。那么最后Virtual DOM如何映射到真实的DOM节点上呢?vue为平台做了一层适配层,浏览器平台见runtime/node-ops.js、weex平台见runtime/node-ops.js,小程序见runtime/node-ops.js。不同平台之间通过适配层对外提供相同的接口,Virtual DOM进行操作Real DOM节点的时候,只需要调用这些适配层的接口即可,而内部实现则不需要关心,它会根据平台的改变而改变。
所以思路肯定是往增加一个mp平台的runtime方向走。但问题是小程序不能操作DOM,所以mp下的node-ops.js
里面的实现都是直接return obj
。
新Virtual DOM和旧Virtual DOM之间需要做一个patch,找出diff。patch完了之后的diff怎么更新视图,也就是如何给这些DOM加入attr、class、style等DOM属性呢?vue中有nextTick的概念用以更新视图,mpvue这块对于小程序的setData
应该怎么处理呢?
另外个问题在于小程序的Virtual DOM怎么生成?也就是怎么将template编译成render function
。这当中还涉及到运行时-编译器-vs-只包含运行时,显然如果要提高性能、减少包大小、输出wxml、mpvue也要提供预编译的能力。因为要预输出wxml且没法动态改变DOM,所以动态组件,自定义 render,和<script type="text/x-template">
字符串模版等都不支持(参考)。
另外还有一些其他问题,最后总结一下
- 1.如何预编译生成
render function
- 2.如何预编译生成wxml,wxss,wxs
- 3.如何patch出diff
- 4.如何更新视图
- 5.如何建立小程序事件代理机制,在事件代理函数中触发与之对应的vue组件事件响应
- 6.如何建立vue实例与小程序 Page 实例关联
- 7.如何建立小程序和vue生命周期映射关系,能在小程序生命周期中触发vue生命周期
platform/mp的目录结构
.
├── compiler //解决问题1,mpvue-template-compiler源码部分
├── runtime //解决问题3 4 5 6 7
├── util //工具方法
├── entry-compiler.js //mpvue-template-compiler的入口。package.json相关命令会自动生成mpvue-template-compiler这个package。
├── entry-runtime.js //对外提供Vue对象,当然是mpvue
└── join-code-in-build.js //编译出SDK时的修复
后面的内容逐步解答这几个问题,也就弄明白了原理
mpvue-loader 是 vue-loader 的一个扩展延伸版,类似于超集的关系,除了vue-loader 本身所具备的能力之外,它还会利用mpvue-template-compiler生成render function
。
- entry
它会从 webpack
的配置中的 entry 开始,分析依赖模块,并分别打包。在entry 中 app 属性及其内容会被打包为微信小程序所需要的 app.js/app.json/app.wxss,其余的会生成对应的页面page.js/page.json/page.wxml/page.wxss,如示例的 entry 将会生成如下这些文件,文件内容下文慢慢讲来:
// webpack.config.js
{
// ...
entry: {
app: resolve('./src/main.js'), // app 字段被识别为 app 类型
index: resolve('./src/pages/index/main.js'), // 其余字段被识别为 page 类型
'news/home': resolve('./src/pages/news/home/index.js')
}
}
// 产出文件的结构
.
├── app.js
├── app.json
├──· app.wxss
├── components
│ ├── card$74bfae61.wxml
│ ├── index$023eef02.wxml
│ └── news$0699930b.wxml
├── news
│ ├── home.js
│ ├── home.wxml
│ └── home.wxss
├── pages
│ └── index
│ ├── index.js
│ ├── index.wxml
│ └── index.wxss
└── static
├── css
│ ├── app.wxss
│ ├── index.wxss
│ └── news
│ └── home.wxss
└── js
├── app.js
├── index.js
├── manifest.js
├── news
│ └── home.js
└── vendor.js
- wxml
每一个
.vue
的组件都会被生成为一个 wxml 规范的 template,然后通过 wxml 规范的 import 语法来达到一个复用,同时组件如果涉及到 props 的 data 数据,我们也会做相应的处理,举个实际的例子:
<template>
<div class="my-component" @click="test">
<h1>{{msg}}</h1>
<other-component :msg="msg"></other-component>
</div>
</template>
<script>
import otherComponent from './otherComponent.vue'
export default {
components: { otherComponent },
data () {
return { msg: 'Hello Vue.js!' }
},
methods: {
test() {}
}
}
</script>
这样一个 vue 的组件的模版部分会生成相应的 wxml
<import src="components/other-component$hash.wxml" />
<template name="component$hash">
<view class="my-component" bindtap="handleProxy">
<view class="_h1">{{msg}}</view>
<template is="other-component$hash" wx:if="{{ $c[0] }}" data="{{ ...$c[0] }}"></template>
</view>
</template>
可能已经注意到了 other-component(:msg="msg") 被转化成了 。mpvue 在运行时会从根组件开始把所有的组件实例数据合并成一个树形的数据,然后通过 setData 到 appData,$c
是 $children 的缩写。至于那个 0 则是我们的 compiler 处理过后的一个标记,会为每一个子组件打一个特定的不重复的标记。 树形数据结构如下:
// 这儿数据结构是一个数组,index 是动态的
{
$child: {
'0'{
// ... root data
$child: {
'0': {
// ... data
msg: 'Hello Vue.js!',
$child: {
// ...data
}
}
}
}
}
}
- wxss
这个部分的处理同 web 的处理差异不大,唯一不同在于通过配置生成 .css 为 .wxss ,其中的对于 css 的若干处理,在 postcss-mpvue-wxss 和 px2rpx-loader 这两部分的文档中又详细的介绍。
app.json/page.json 1.1.1 以上
推荐和小程序一样,将 app.json/page.json 放到页面入口处,使用 copy-webpack-plugin copy 到对应的生成位置。
1.1.1 以下
这部分内容来源于 app 和 page 的 entry 文件,通常习惯是 main.js,你需要在你的入口文件中 export default { config: {} },这才能被我们的 loader 识别为这是一个配置,需要写成 json 文件。
import Vue from 'vue';
import App from './app';
const vueApp = new Vue(App);
vueApp.$mount();
// 这个是我们约定的额外的配置
export default {
// 这个字段下的数据会被填充到 app.json / page.json
config: {
pages: ['static/calendar/calendar', '^pages/list/list'], // Will be filled in webpack
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#455A73',
navigationBarTitleText: '美团汽车票',
navigationBarTextStyle: '#fff'
}
}
};
同时,这个时候,我们会根据 entry 的页面数据,自动填充到 app.json 中的 pages 字段。 pages 字段也是可以自定义的,约定带有 ^ 符号开头的页面,会放到数组的最前面。
style scoped 在 vue-loader 中对 style scoped 的处理方式是给每个样式加一个 attr 来标记 module-id,然后在 css 中也给每条 rule 后添加 [module-id],最终可以形成一个 css 的“作用域空间”。
在微信小程序中目前是不支持 attr 选择器的,所以我们做了一点改动,把 attr 上的 [module-id] 直接写到了 class 里,如下:
<!-- .vue -->
<template>
<div class="container">
// ...
</div>
</template>
<style scoped>
.container {
color: red;
}
</style>
<!-- vue-loader -->
<template>
<div class="container" data-v-23e58823>
// ...
</div>
</template>
<style scoped>
.container[data-v-23e58823] {
color: red;
}
</style>
<!-- mpvue-loader -->
<template>
<div class="container data-v-23e58823">
// ...
</div>
</template>
<style scoped>
.container.data-v-23e58823 {
color: red;
}
</style>
- compiler
生产出的内容是:
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// mpvue-template-compiler会利用AST预编译生成一个render function用以生成Virtual DOM。
var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
// _c创建虚拟节点,参考https://github.com/Meituan-Dianping/mpvue/blob/master/packages/mpvue/index.js#L3606
// 以及https://github.com/Meituan-Dianping/mpvue/blob/master/packages/mpvue/index.js#L3680
return _c('div', {
staticClass: "my-component"
}, [_c('h1', [_vm._v(_vm._s(_vm.msg))]), _vm._v(" "), _c('other-component', {
attrs: {
"msg": _vm.msg,
"mpcomid": '0'
}
})], 1)
}
// staticRenderFns的作用是静态渲染,在更新时不会进行patch,优化性能。而staticRenderFns是个空数组。
var staticRenderFns = []
render._withStripped = true
var esExports = { render: render, staticRenderFns: staticRenderFns }
/* harmony default export */ __webpack_exports__["a"] = (esExports);
if (false) {
module.hot.accept()
if (module.hot.data) {
require("vue-hot-reload-api").rerender("data-v-54ad9125", esExports)
}
}
/***/ })
compiler相关,也就是template预编译这块,可以参考《聊聊Vue的template编译》来搞明白。原理是一样的。
mpvue自己实现了export { compile, compileToFunctions, compileToWxml }
(链接)其中compileToWxml
是用来生成wxml,具体代码在这。
另外mpvue是不需要提供运行时-编译器的,虽然理论上是能够做到的。因为小程序不能操作DOM,即便提供了运行时-编译器也产生不了界面。
详细讲解compile过程:
1.将vue文件解析成模板对象
// mpvue-loader/lib/loader.js
var parts = parse(content, fileName, this.sourceMap)
假如vue文件源码如下:
<template>
<view class="container-bg">
<view class="home-container">
<home-quotation-view v-for="(item, index) in lists" :key="index" :reason="item.reason" :stockList="item.list" @itemViewClicked="itemViewClicked" />
</view>
</view>
</template>
<script lang="js">
import homeQuotationView from '@/components/homeQuotationView'
import topListApi from '@/api/topListApi'
export default {
data () {
return {
lists: []
}
},
components: {
homeQuotationView
},
methods: {
async loadRankList () {
let {data} = await topListApi.rankList()
if (data) {
this.dateTime = data.dt
this.lists = data.rankList.filter((item) => {
return !!item
})
}
},
itemViewClicked (quotationItem) {
wx.navigateTo({
url: `/pages/topListDetail/main?item=${JSON.stringify(quotationItem)}`
})
}
},
onShow () {
this.loadRankList()
}
}
</script>
<style lang="stylus" scoped>
.container-bg
width 100%
height 100%
background-color #F2F4FA
.home-container
width 100%
height 100%
overflow-x hidden
</style>
调用parse(content, fileName, this.sourceMap)
函数得到的结果大致如下:
{
template: {
type: 'template',
content: '\n<view class="container-bg">\n <view class="home-container">\n <home-quotation-view v-for="(item, index) in lists" :key="index" :reason="item.reason" :stockList="item.list" @itemViewClicked="itemViewClicked" />\n </view>\n</view>\n',
start: 10,
attrs: {},
end: 251
},
script: {
type: 'script',
content: '\n\n\n\n\n\n\n\n\nimport homeQuotationView from \'@/components/homeQuotationView\'\nimport topListApi from \'@/api/topListApi\'\n\nexport default {\n data () {\n return {\n lists: []\n }\n },\n components: {\n homeQuotationView\n },\n methods: {\n async loadRankList () {\n let {data} = await topListApi.rankList()\n if (data) {\n this.dateTime = data.dt\n this.lists = data.rankList.filter((item) => {\n return !!item\n })\n }\n },\n itemViewClicked (quotationItem) {\n wx.navigateTo({\n url: `/pages/topListDetail/main?item=${JSON.stringify(quotationItem)}`\n })\n }\n },\n onShow () {\n this.loadRankList()\n }\n}\n',
start: 282,
attrs: {
lang: 'js'
},
lang: 'js',
end: 946,
...
},
styles: [{
type: 'style',
content: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.container-bg\n width 100%\n height 100%\n background-color #F2F4FA\n\n.home-container\n width 100%\n height 100%\n overflow-x hidden\n\n',
start: 985,
attrs: [Object],
lang: 'stylus',
scoped: true,
end: 1135,
...
}],
customBlocks: []
}
2.调用mpvue-loader/lib/template-compiler/index.js导出的接口并传入上面得到的html模板:
var templateCompilerPath = normalize.lib('template-compiler/index')
...
var defaultLoaders = {
html: templateCompilerPath + templateCompilerOptions,
css: options.extractCSS
? getCSSExtractLoader()
: styleLoaderPath + '!' + 'css-loader' + cssLoaderOptions,
js: hasBuble ? ('buble-loader' + bubleOptions) : hasBabel ? babelLoaderOptions : ''
}
// check if there are custom loaders specified via
// webpack config, otherwise use defaults
var loaders = Object.assign({}, defaultLoaders, options.loaders)
- 调用mpvue/packages/mpvue-template-compiler/build.js的compile接口:
// mpvue-loader/lib/template-compiler/index.js
var compiled = compile(html, compilerOptions)
compile方法生产下面的ast(Abstract Syntax Tree)模板,render函数和staticRenderFns
{
ast: {
type: 1,
tag: 'view',
attrsList: [],
attrsMap: {
class: 'container-bg'
},
parent: undefined,
children: [{
type: 1,
tag: 'view',
attrsList: [],
attrsMap: {
class: 'home-container'
},
parent: {
type: 1,
tag: 'view',
attrsList: [],
attrsMap: {
class: 'container-bg'
},
parent: undefined,
children: [
[Circular]
],
plain: false,
staticClass: '"container-bg"',
static: false,
staticRoot: false
},
children: [{
type: 1,
tag: 'home-quotation-view',
attrsList: [{
name: ':reason',
value: 'item.reason'
}, {
name: ':stockList',
value: 'item.list'
}, {
name: '@itemViewClicked',
value: 'itemViewClicked'
}],
attrsMap: {
'v-for': '(item, index) in lists',
':key': 'index',
':reason': 'item.reason',
':stockList': 'item.list',
'@itemViewClicked': 'itemViewClicked',
'data-eventid': '{{\'0-\'+index}}',
'data-comkey': '{{$k}}'
},
parent: [Circular],
children: [],
for: 'lists',
alias: 'item',
iterator1: 'index',
key: 'index',
plain: false,
hasBindings: true,
attrs: [{
name: 'reason',
value: 'item.reason'
}, {
name: 'stockList',
value: 'item.list'
}, {
name: 'eventid',
value: '\'0-\'+index'
}, {
name: 'mpcomid',
value: '\'0-\'+index'
}],
events: {
itemViewClicked: {
value: 'itemViewClicked',
modifiers: undefined
}
},
eventid: '\'0-\'+index',
mpcomid: '\'0-\'+index',
static: false,
staticRoot: false,
forProcessed: true
}],
plain: false,
staticClass: '"home-container"',
static: false,
staticRoot: false
}],
plain: false,
staticClass: '"container-bg"',
static: false,
staticRoot: false
},
render: 'with(this){return _c(\'view\',{staticClass:"container-bg"},[_c(\'view\',{staticClass:"home-container"},_l((lists),function(item,index){return _c(\'home-quotation-view\',{key:index,attrs:{"reason":item.reason,"stockList":item.list,"eventid":\'0-\'+index,"mpcomid":\'0-\'+index},on:{"itemViewClicked":itemViewClicked}})}))])}',
staticRenderFns: [],
errors: [],
tips: []
}
其中的render函数运行的结果是返回VNode
对象,其实render
函数应该长下面这样:
(function() {
with(this){
return _c('div',{ //创建一个 div 元素
attrs:{"id":"app"} //div 添加属性 id
},[
_m(0), //静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render 函数
_v(" "), //空的文本节点
(message) //三元表达式,判断 message 是否存在
//如果存在,创建 p 元素,元素里面有文本,值为 toString(message)
?_c('p',[_v("\n "+_s(message)+"\n ")])
//如果不存在,创建 p 元素,元素里面有文本,值为 No message.
:_c('p',[_v("\n No message.\n ")])
]
)
}
})
其中的_c
就是vue对象的createElement
方法 (创建元素),_m
是renderStatic
(渲染静态节点),_v
是
createTextVNode
(创建文本dom),_s
是 toString
(转换为字符串)
// src/core/instance/render.js
export function initRender (vm: Component) {
...
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
...
}
...
Vue.prototype._s = toString
...
Vue.prototype._m = renderStatic
...
Vue.prototype._v = createTextVNode
...
- 调用compileWxml方法生产wxml模板,这个方法最终会调用 mpvue/packages/mpvue-template-compiler/build.js的compileToWxml方法将第一步compile出来的模板转成小程序的wxml模板
// mpvue-loader/lib/template-compiler/index.js
compileToWxml.call(this, compiled, html)
以上解答了问题1、2
目录结构
.
├── events.js //解答问题5
├── index.js //入口提供Vue对象,以及$mount,和各种初始化
├── liefcycle //解答问题6、7
├── node-ops.js //操作真实DOM的相关实现,因为小程序不能操作DOM,所以这里都是直接返回
├── patch.js //解答问题3
└── render.js //解答问题4
和vue使用的createPatchFunction
保持一致,任然是旧树和新树进行patch产出diff,但是多了一行this.$updateDataToMP()用以更新。
两个核心的方法initDataToMP
、updateDataToMP
。
initDataToMP
收集vm上的data,然后调用小程序Page示例的setData
渲染。
updateDataToMP
在每次patch,也就是依赖收集发现数据改变时更新(参考patch.js代码),这部分一样会使用nextTick
和队列。最终使用了节流阀throttleSetData
。50毫秒用来控制频率以解决频繁修改Data,会造成大量传输Data数据而导致的性能问题。
其中collectVmData
最终也是用到了formatVmData
。尤其要注意的是一句注释:
getVmData 这儿获取当前组件内的所有数据,包含 props、computed 的数据
我们又知道,service到view是两个线程间通信,如果Data含有大量数据,增加了传输数据量,加大了传输成本,将会造成性能下降。
正如官网所说的,这里使用eventTypeMap
做了各事件的隐射
import { getComKey, eventTypeMap } from '../util/index'
// 用于小程序的 event type 到 web 的 event
export const eventTypeMap = {
tap: ['tap', 'click'],
touchstart: ['touchstart'],
touchmove: ['touchmove'],
touchcancel: ['touchcancel'],
touchend: ['touchend'],
longtap: ['longtap'],
input: ['input'],
blur: ['change', 'blur'],
submit: ['submit'],
focus: ['focus'],
scrolltoupper: ['scrolltoupper'],
scrolltolower: ['scrolltolower'],
scroll: ['scroll']
}
使用了handleProxyWithVue
方法来代理小程序事件到vue事件。
另外看下作者自己对这部分的思路
事件代理机制:用户交互触发的数据更新通过事件代理机制完成。在 Vue.js 代码中,事件响应函数对应到组件的 method, Vue.js 自动维护了上下文环境。然而在小程序中并没有类似的机制,又因为 Vue.js 执行环境中维护着一份实时的虚拟 DOM,这与小程序的视图层完全对应,我们思考,在小程序组件节点上触发事件后,只要找到虚拟 DOM 上对应的节点,触发对应的事件不就完成了么;另一方面,Vue.js 事件响应如果触发了数据更新,其生命周期函数更新将自动触发,在此函数上同步更新小程序数据,数据同步也就实现了。
getHandle
这个方法应该就是作者思路当中所说的:找到对应节点,然后找到handle。
在initMP
方法中,自己创建小程序的App、Page。实现生命周期相关方法,使用callHook
代理兼容小程序App、Page的生命周期。
官方文档生命周期中说到了:
同 vue,不同的是我们会在小程序 onReady 后,再去触发 vue mounted 生命周期
这部分查看,onReady
之后才会执行next
,这个next
回调最终是vue的mountComponent
。可以在index.js中看到。这部分代码也就是解决了"小程序生命周期中触发vue生命周期"。
export function initMP (mpType, next) {
// ...
global.Page({
// 生命周期函数--监听页面初次渲染完成
onReady () {
mp.status = 'ready'
callHook(rootVueVM, 'onReady')
next()
},
})
// ...
}
在小程序onShow时,使用$nextTick去第一次渲染数据,参考上面提到的render.js。
export function initMP (mpType, next) {
// ...
global.Page({
// 生命周期函数--监听页面显示
onShow () {
mp.page = this
mp.status = 'show'
callHook(rootVueVM, 'onShow')
// 只有页面需要 setData
rootVueVM.$nextTick(() => {
rootVueVM._initDataToMP()
})
},
})
// ...
}
在mpvue-loader生成template时,比如点击事件@click
会变成bindtap="handleProxy"
,事件绑定全都会使用handleProxy
这个方法。
可以查看上面mpvue-loader回顾一下。
最终handleProxy调用的是event.js中的handleProxyWithVue
。
export function initMP (mpType, next) {
// ...
global.Page({
handleProxy (e) {
return rootVueVM.$handleProxyWithVue(e)
},
})
// ...
}
最后index.js就负责各种初始化和mount。
原因:目前的组件是使用小程序的 template 标签实现的,给组件指定的class和style是挂载在template标签上,而template 标签不支持 class 及 style 属性。
解决方案: 在自定义组件上绑定class或style到一个props属性上。
// 组件ComponentA.vue
<template>
<div class="container" :class="pClass">
...
</div>
</template>
<script>
export default {
props: {
pClass: {
type: String,
default: ''
}
}
}
</script>
<!--PageB.vue-->
<template>
<component-a :pClass="cusComponentAClass" />
</template>
<script>
data () {
return {
cusComponentAClass: 'a-class b-class'
}
}
</script>
<style lang="stylus" scoped>
.a-class
border red solid 2rpx
.b-class
margin-right 20rpx
</style>
但是这样会有问题就是style加上scoped之后,编译模板生成的代码是下面这样的:
.a-class.data-v-8f1d914e {
border: #f00 solid 2rpx;
}
.b-class.data-v-8f1d914e {
margin-right 20rpx
}
所以想要这些组件的class生效就不能使用scoped的style,改成下面这样,最好自己给a-class和b-class加前缀以防其他的文件引用这些样式:
<style lang="stylus">
.a-class
border red solid 2rpx
.b-class
margin-right 20rpx
</style>
<style lang="stylus" scoped>
.other-class
border red solid 2rpx
...
</style>
- 在定义组件上绑定style属性到一个props属性上:
<!--P组件ComponentA.vue-->
<template>
<div class="container" :style="pStyle">
...
</div>
</template>
<script>
export default {
props: {
pStyle: {
type: String,
default: ''
}
}
}
</script>
<!--PageB.vue-->
<template>
<component-a :pStyle="cusComponentAStyle" />
</template>
<script>
const cusComponentAStyle = 'border:red solid 2rpx; margin-right:20rpx;'
data () {
return {
cusComponentAStyle
}
}
</script>
<style lang="stylus" scoped>
...
</style>
也可以通过定义styleObject,然后通过工具函数转化为styleString,如下所示:
const bstyle = {
border: 'red solid 2rpx',
'margin-right': '20rpx'
}
let arr = []
for (let [key, value] of Object.entries(bstyle)) {
arr.push(`${key}: ${value}`)
}
const cusComponentAStyle = arr.join('; ')
- 当然自定义组件确定只会改变某个css样式,通过pros传入单个样式的值,然后通过:style绑定肯定没问题:
<!--组件ComponentA.vue-->
<template>
<div class="container" :style="{'background-color': backgroundColor}">
...
</div>
</template>
<script>
export default {
props: {
backgroundColor: {
type: String,
default: 'yellow'
}
}
}
</script>
<!-- PageB.vue -->
<template>
<component-a backgroundColor="red" />
</template>
package.json修改
- 升级: "mpvue-loader": "^1.1.2-rc.4" "webpack-mpvue-asset-plugin": "^0.1.1"
- 新增: "relative": "^3.0.2"
注意事项
- 1.1.2-rc.5 修复 slot 文件路径生成错误的问题
- 1.1.x 版本还不是很稳定,对稳定性要求较高的项目建议暂时使用 1.0.x 版本
移动src/main.js中config相关内容到同级目录下main.json(新建)中
export default {
// config: {...} 需要移动
}
to
{
"pages": [
"pages/index/main",
"pages/logs/main"
],
"subPackages": [
{
"root": "pages/packageA",
"pages": [
"counter/main"
]
}
],
"window": {...}
}
webpack 配置配合升级指南
- 本次升级意在调整生成文件目录结构,对依赖的文件由原来的写死绝对路径该改为相对路径
- mpvue-loader@1.1.2-rc.4 依赖 webpack-mpvue-asset-plugin@0.1.0 做依赖资源引用
- 之前写在 main.js 中的 config 信息,需要在 main.js 同级目录下新建 main.json 文件,使用 webapck-copy-plugin copy 到 build 目录下
- app.json 中引用的图片不会自动 copy 到 dist 目录下 json 配置文件是由 webapck-copy-plugin copy 过去的,不会处理依赖,可以将图片放到根目录下 static 目录下,使用 webapck-copy-plugin copy 过去
build/webpack.base.conf.js
+var CopyWebpackPlugin = require('copy-webpack-plugin')
+var relative = require('relative')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
-function getEntry (rootSrc, pattern) {
- var files = glob.sync(path.resolve(rootSrc, pattern))
- return files.reduce((res, file) => {
- var info = path.parse(file)
- var key = info.dir.slice(rootSrc.length + 1) + '/' + info.name
- res[key] = path.resolve(file)
- return res
- }, {})
+function getEntry (rootSrc) {
+ var map = {};
+ glob.sync(rootSrc + '/pages/**/main.js')
+ .forEach(file => {
+ var key = relative(rootSrc, file).replace('.js', '');
+ map[key] = file;
+ })
+ return map;
}
plugins: [
- new MpvuePlugin()
+ new MpvuePlugin(),
+ new CopyWebpackPlugin([{
+ from: '**/*.json',
+ to: 'app.json'
+ }], {
+ context: 'src/'
+ }),
+ new CopyWebpackPlugin([ // 处理 main.json 里面引用的图片,不要放代码中引用的图片
+ {
+ from: path.resolve(__dirname, '../static'),
+ to: path.resolve(__dirname, '../dist/static'),
+ ignore: ['.*']
+ }
+ ])
]
}
build/webpack.dev.conf.js
module.exports = merge(baseWebpackConfig, {
devtool: '#source-map',
output: {
path: config.build.assetsRoot,
- filename: utils.assetsPath('js/[name].js'),
- chunkFilename: utils.assetsPath('js/[id].js')
+ filename: utils.assetsPath('[name].js'),
+ chunkFilename: utils.assetsPath('[id].js')
},
plugins: [
new webpack.DefinePlugin({
module.exports = merge(baseWebpackConfig, {
// copy from ./webpack.prod.conf.js
// extract css into its own file
new ExtractTextPlugin({
- filename: utils.assetsPath('css/[name].wxss')
+ filename: utils.assetsPath('[name].wxss')
}),
module.exports = merge(baseWebpackConfig, {
}
}),
new webpack.optimize.CommonsChunkPlugin({
- name: 'vendor',
+ name: 'common/vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.exports = merge(baseWebpackConfig, {
}
}),
new webpack.optimize.CommonsChunkPlugin({
- name: 'manifest',
- chunks: ['vendor']
+ name: 'common/manifest',
+ chunks: ['common/vendor']
}),
- // copy custom static assets
- new CopyWebpackPlugin([
- {
- from: path.resolve(__dirname, '../static'),
- to: config.build.assetsSubDirectory,
- ignore: ['.*']
- }
- ]),
build/webpack.prod.conf.js
var webpackConfig = merge(baseWebpackConfig, {
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {
path: config.build.assetsRoot,
- filename: utils.assetsPath('js/[name].js'),
- chunkFilename: utils.assetsPath('js/[id].js')
+ filename: utils.assetsPath('[name].js'),
+ chunkFilename: utils.assetsPath('[id].js')
},
plugins: [
var webpackConfig = merge(baseWebpackConfig, {
}),
// extract css into its own file
new ExtractTextPlugin({
- // filename: utils.assetsPath('css/[name].[contenthash].css')
- filename: utils.assetsPath('css/[name].wxss')
+ // filename: utils.assetsPath('[name].[contenthash].css')
+ filename: utils.assetsPath('[name].wxss')
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
var webpackConfig = merge(baseWebpackConfig, {
new webpack.HashedModuleIdsPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
- name: 'vendor',
+ name: 'common/vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
var webpackConfig = merge(baseWebpackConfig, {
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
- name: 'manifest',
- chunks: ['vendor']
- }),
+ name: 'common/manifest',
+ chunks: ['common/vendor']
+ })
- // copy custom static assets
- new CopyWebpackPlugin([
- {
- from: path.resolve(__dirname, '../static'),
- to: config.build.assetsSubDirectory,
- ignore: ['.*']
- }
- ])
]
})
config/index.js
module.exports = {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
- assetsSubDirectory: 'static', // 不将资源聚合放在 static 目录下
+ assetsSubDirectory: '',
assetsPublicPath: '/',
productionSourceMap: false,
// Gzip off by default as many popular static hosts such as
@@ -26,7 +26,7 @@ module.exports = {
port: 8080,
// 在小程序开发者工具中不需要自动打开浏览器
autoOpenBrowser: false,
- assetsSubDirectory: 'static', // 不将资源聚合放在 static 目录下
+ assetsSubDirectory: '',
assetsPublicPath: '/',
proxyTable: {},
// CSS Sourcemaps off by default because relative paths are "buggy"
以上内容部分来自: