wepy是一个优秀的微信小程序组件化框架,突破了小程序的限制,支持了npm包加载以及组件化方案,而且在性能优化方面也下了功夫,无论以后项目是否会使用到,该框架组件化编译方案都是值得学习和深刻的。
javascript
本文同步于我的博客 www.imhjm.com/article/597…
wepy文档: wepyjs.github.io/wepy/#/
github地址:github.com/wepyjs/wepy前端
咱们先提出几个问题,让下面的分析更有针对性:java
咱们能够先clone下wepy的github目录node
docsify
生成的文档网站咱们就重点看wepy以及wepy-cli,接下来的文章也是围绕这两个展开
git
require('../lib/wepy');复制代码
下图简单画出了总体wepy-cli的编译构建流程,忽略了部分细节
github
<script src="">
),调用compile方法开始编译,若是指定文件,则相应判断寻找父组件或者寻找引用去编译compile-wpy调用resolveWpy方法(核心)shell
xmldom
放入内容,操做节点rst.script.code = rst.script.code.replace(/[\s\r\n]components\s*=[\s\r\n]*/, (match, item, index) => {
return `$props = ${JSON.stringify(props)};\r\n$events = ${JSON.stringify(events)};\r\n${match}`;
});复制代码
let rst = {
moduleId: moduleId,
style: [],
template: {
code: '',
src: '',
type: '',
components: {},
},
script: {
code: '',
src: '',
type: '',
},
config: {},
};复制代码
rst构建完成,开始逐个操做express
xmldom
rst.scriptnpm
假如不是npm包,则置入wepy框架初始化代码json
if (type !== 'npm') {
if (type === 'page' || type === 'app') {
code = code.replace(/exports\.default\s*=\s*(\w+);/ig, function (m, defaultExport) {
if (defaultExport === 'undefined') {
return '';
}
if (type === 'page') {
let pagePath = path.join(path.relative(appPath.dir, opath.dir), opath.name).replace(/\\/ig, '/');
return `\nPage(require('wepy').default.$createPage(${defaultExport} , '${pagePath}'));\n`;
} else {
appPath = opath;
let appConfig = JSON.stringify(config.appConfig);
return `\nApp(require('wepy').default.$createApp(${defaultExport}, ${appConfig}));\n`;
}
});
}
}复制代码
大致就像开发文档的图同样,如今看就很清晰了
这个方法用于生成rst,拆分wpy单文件组件,上面流程讲了大部分,这里就详细讲下props和event的提取
其实也不是很复杂,就是遍历元素,取出相应attributes,放入events[comid][attr.name]
以及props[comid][attr.name]
放入代码中
elems.forEach((elem) => {
// ignore the components calculated in repeat.
if (calculatedComs.indexOf(elem) === -1) {
let comid = util.getComId(elem);
[].slice.call(elem.attributes || []).forEach((attr) => {
if (attr.name !== 'id' && attr.name !== 'path') {
if (/v-on:/.test(attr.name)) { // v-on:fn user custom event
if (!events[comid])
events[comid] = {};
events[comid][attr.name] = attr.value;
} else {
if (!props[comid])
props[comid] = {};
if (['hidden', 'wx:if', 'wx:elif', 'wx:else'].indexOf(attr.name) === -1) {
props[comid][attr.name] = attr.value;
}
}
}
});
}
});
if (Object.keys(props).length) {
rst.script.code =rst.script.code.replace(/[\s\r\n]components\s*=[\s\r\n]*/, (match, item, index) => {
return `$props = ${JSON.stringify(props)};\r\n$events = ${JSON.stringify(events)};\r\n${match}`;
});
}复制代码
//... util.geComId
getComId(elem) {
let tagName = elem.nodeName;
let path = elem.getAttribute('path');
let id = elem.getAttribute('id');
if (tagName !== 'component')
return tagName;
if (id)
return id;
if (path && !id)
return path;
},复制代码
下面精简了下代码,易于理解
updateBind(node, prefix, ignores = {}, mapping = {}) {
let comid = prefix;
if (node.nodeName === '#text' && prefix) {
if (node.data && node.data.indexOf('{{') > -1) {
node.replaceData(0, node.data.length, this.parseExp(node.data, prefix, ignores, mapping));
}
} else {
[].slice.call(node.attributes || []).forEach((attr) => {
if (prefix) {
if (attr.value.indexOf('{{') > -1) {
attr.value = this.parseExp(attr.value, prefix, ignores, mapping);
}
}
if (attr.name.indexOf('bind') === 0 || attr.name.indexOf('catch') === 0) {
if (prefix) {
attr.value = `$${comid}$${attr.value}`;
}
}
});
[].slice.call(node.childNodes || []).forEach((child) => {
this.updateBind(child, prefix, ignores, mapping);
});
}
},复制代码
parseExp(content, prefix, ignores, mapping) {
let comid = prefix;
// replace {{ param ? 'abc' : 'efg' }} => {{ $prefix_param ? 'abc' : 'efg' }}
return content.replace(/\{\{([^}]+)\}\}/ig, (matchs, words) => {
return matchs.replace(/[^\.\w'"](\.{0}|\.{3})([a-z_\$][\w\d\._\$]*)/ig, (match, expand, word, n) => {
// console.log(matchs + '------' + match + '--' + word + '--' + n);
let char = match[0];
let tmp = word.match(/^([\w\$]+)(.*)/);
let w = tmp[1];
let rest = tmp[2];
if (ignores[w] || this.isInQuote(matchs, n)) {
return match;
} else {
if (mapping.items && mapping.items[w]) {
// prefix 减小一层
let upper = comid.split(PREFIX);
upper.pop();
upper = upper.join(PREFIX);
upper = upper ? `${PREFIX}${upper}${JOIN}` : '';
return `${char}${expand}${upper}${mapping.items[w].mapping}${rest}`;
}
return `${char}${expand}${PREFIX}${comid}${JOIN}${word}`;
}
});
});
},复制代码
这个方法用于wpy框架的加载机制
将require部分替换成正确的编译后的路径
npm包经过读取相应package.json中的main部分去寻找文件,寻找npm文件会再继续resolveDeps获取依赖,最后写入npm中
resolveDeps (code, type, opath) {
let params = cache.getParams();
let wpyExt = params.wpyExt;
return code.replace(/require\(['"]([\w\d_\-\.\/@]+)['"]\)/ig, (match, lib) => {
let resolved = lib;
let target = '', source = '', ext = '', needCopy = false;
if (lib[0] === '.') { // require('./something'');
source = path.join(opath.dir, lib); // e:/src/util
if (type === 'npm') {
target = path.join(npmPath, path.relative(modulesPath, source));
needCopy = true;
} else {
// e:/dist/util
target = util.getDistPath(source);
needCopy = false;
}
} else if (lib.indexOf('/') === -1 || // require('asset');
lib.indexOf('/') === lib.length - 1 || // reqiore('a/b/something/')
(lib[0] === '@' && lib.indexOf('/') !== -1 && lib.lastIndexOf('/') === lib.indexOf('/')) // require('@abc/something')
) {
let pkg = this.getPkgConfig(lib);
if (!pkg) {
throw Error('找不到模块: ' + lib);
}
let main = pkg.main || 'index.js';
if (pkg.browser && typeof pkg.browser === 'string') {
main = pkg.browser;
}
source = path.join(modulesPath, lib, main);
target = path.join(npmPath, lib, main);
lib += path.sep + main;
ext = '';
needCopy = true;
} else { // require('babel-runtime/regenerator')
//console.log('3: ' + lib);
source = path.join(modulesPath, lib);
target = path.join(npmPath, lib);
ext = '';
needCopy = true;
}
if (util.isFile(source + wpyExt)) {
ext = '.js';
} else if (util.isFile(source + '.js')) {
ext = '.js';
} else if (util.isDir(source) && util.isFile(source + path.sep + 'index.js')) {
ext = path.sep + 'index.js';
}else if (util.isFile(source)) {
ext = '';
} else {
throw ('找不到文件: ' + source);
}
source += ext;
target += ext;
lib += ext;
resolved = lib;
// 第三方组件
if (/\.wpy$/.test(resolved)) {
target = target.replace(/\.wpy$/, '') + '.js';
resolved = resolved.replace(/\.wpy$/, '') + '.js';
lib = resolved;
}
if (needCopy) {
if (!cache.checkBuildCache(source)) {
cache.setBuildCache(source);
util.log('依赖: ' + path.relative(process.cwd(), target), '拷贝');
// 这里是写入npm包,而且继续寻找依赖的地方
this.compile('js', null, 'npm', path.parse(source));
}
}
if (type === 'npm') {
if (lib[0] !== '.') {
resolved = path.join('..' + path.sep, path.relative(opath.dir, modulesPath), lib);
} else {
if (lib[0] === '.' && lib[1] === '.')
resolved = './' + resolved;
}
} else {
resolved = path.relative(util.getDistPath(opath, opath.ext, src, dist), target);
}
resolved = resolved.replace(/\\/g, '/').replace(/^\.\.\//, './');
return `require('${resolved}')`;
});
},复制代码
在代码中会常看到如下PluginHelper再进行写入,咱们能够看看如何实现plugin一个一个运用到content中的
let plg = new loader.PluginHelper(config.plugins, {
type: 'wxml',
code: util.decode(node.toString()),
file: target,
output (p) {
util.output(p.action, p.file);
},
done (rst) {
util.output('写入', rst.file);
rst.code = self.replaceBooleanAttr(rst.code);
util.writeFile(target, rst.code);
}
});复制代码
核心代码以下,其实跟koa/express中间的compose相似,经过next方法,调用完一个调用下一个next(),next()不断,最后done(),next方法在框架内部实现,done方法有咱们配置便可,固然在插件中(就像中间件)须要在最后调用next
class PluginHelper {
constructor (plugins, op) {
this.applyPlugin(0, op);
return true;
}
applyPlugin (index, op) {
let plg = loadedPlugins[index];
if (!plg) {
op.done && op.done(op);
} else {
op.next = () => {
this.applyPlugin(index + 1, op);
};
op.catch = () => {
op.error && op.error(op);
};
if (plg)
plg.apply(op);
}
}
}复制代码
这里的wepy是wepy框架的前端部分,须要在小程序中import的
主要职责就是让框架中props和events能成功使用,就是须要setData一些加prefix的内容,而且实现组件之间的通讯,以及部分性能调优
针对前端wepy部分,也画了个流程图方便理解,也略去大量细节部分,后面分析能够跟着图来
// page
Page(require('wepy').default.$createPage(${defaultExport} , '${pagePath}'));
// app
App(require('wepy').default.$createApp(${defaultExport}, ${appConfig}));复制代码
这也是入口所在,从这里开始入手分析$createApp在App包裹中,正常小程序应该是App({}),因此这里$createApp返回config,这里new class extends wepy.app, 经过调用app.js中$initAPI实现接口promise化以及实现拦截器
定义接口使用
Object.defineProperty(native, key, {
get () { return (...args) => wx[key].apply(wx, args) }
});
wepy[key] = native[key];复制代码
success时候reoslve,fail时候reject实现promise化,在其中查询拦截器调用
if (self.$addons.promisify) {
return new Promise((resolve, reject) => {
let bak = {};
['fail', 'success', 'complete'].forEach((k) => {
bak[k] = obj[k];
obj[k] = (res) => {
if (self.$interceptors[key] && self.$interceptors[key][k]) {
res = self.$interceptors[key][k].call(self, res);
}
if (k === 'success')
resolve(res)
else if (k === 'fail')
reject(res);
};
});
if (self.$addons.requestfix && key === 'request') {
RequestMQ.request(obj);
} else
wx[key](obj);
});
}复制代码
$createPage在Page包裹中,一样返回config{},构造page实例,来自new class extends wepy.page,page class又继承于component
Props.build(this.props);
(注意这个props是前端编写)构建props,并寻找父级的$props(编译注入),获取值之后放在this.data[key]里,若是有props设定为twoWay,一样放入$mappingProps中defaultData[`${this.$prefix}${k}`] = this.data[k];
this[k] = this.data[k];复制代码
$apply方法须要特别提一下,它于component中的$digest配合,是wepy框架里比较核心的脏值检查setData机制
下图是官网的图
$apply (fn) {
if (typeof(fn) === 'function') {
fn.call(this);
this.$apply();
} else {
if (this.$$phase) {
this.$$phase = '$apply';
} else {
this.$digest();
}
}
}复制代码
$digest方法就是脏值检查了,顺便再讲以前的$createPage咱们能够看到data放在好几个地方,this[k],this.data[k],this.$data,这里来区分如下它们
分析完上面,脏值检查就很明了了,拿this.$data跟this中比较,不等的话放入readyToSet中,而后再setData,更新this.$data便可,还需注意上面官网图下两个tips,注意只会有一个脏数据检查流程
至于组件通讯方面,有了一棵组件树,理好层级父级的关系就不复杂了,举一个$emit触发父级事件例子分析一下
假如不是,则一层一层找上级方法emit便可
$emit (evtName, ...args) {
let com = this;
let source = this;
let $evt = new event(evtName, source, 'emit');
// User custom event;
if (this.$parent.$events && this.$parent.$events[this.$name]) {
let method = this.$parent.$events[this.$name]['v-on:' + evtName];
if (method && this.$parent.methods) {
let fn = this.$parent.methods[method];
if (typeof(fn) === 'function') {
this.$parent.$apply(() => {
fn.apply(this.$parent, args.concat($evt));
});
return;
} else {
throw new Error(`Invalid method from emit, component is ${this.$parent.$name}, method is ${method}. Make sure you defined it already.\n`);
}
}
}
while(com && com.$isComponent !== undefined && $evt.active) {
// 保存 com 块级做用域组件实例
let comContext = com;
let fn = getEventsFn(comContext, evtName);
fn && comContext.$apply(() => {
fn.apply(comContext, args.concat($evt));
});
com = comContext.$parent;
}
}复制代码
其余的invoke和broadcast不具体讲了,只要构建出组件树,问题就很好解决(构建就是在每次new的时候记住它的$parent就好了)
到这里,总体流程大体讲完了,没有涵盖全部细节,做者的这个小程序框架很强大,而且做者还在积极地解决issue和更新,值得咱们点赞~
接下来来回答下文首提出的问题,应该就迎刃而解了
1. wepy如何实现单文件组件.wpy编译?
答:wepy框架经过wepy-cli对.wpy编译,拆解为style,script(+config),template几部分,再分别处理,生成到dist文件对应xxx.wxss,xxx.script,xxx.json,xxx.wxml
2. 如何隔离组件做用域?
答:经过组件在不一样page的命名做为前缀,而且以父级为起点,依次为$child,再子级就是$child$chind,依次类推。。。不一样组件在不一样的component实例下,data set到page就是带上前缀,一样的method也是加入前缀放在Page({})中
3. 如何实现组件通讯?
答:经过编译获取component的路径注入代码,在小程序代码运行时,根据逐层require获取,new component,并记下父级$parent,构建组件树。
若是向子组件传props和events?
编译时就会收集在template中传入的props和events注入到代码中$props和$events,而后子组件init的时候获取父级$parent的$props并加入前缀$prefix去setData(子组件的在page中的元素表现已经在编译的时候被替换成了$prefix$data的样子),这样就实现了传值。调用$emit触发父组件event,直接寻找父级$parent apply调用相应方法便可。
广播事件broadcast就是直接广度优先去遍历组件树就好了。
4. 如何实现加载外部npm包?
答:wepy-cli在处理script部分,根据require的内容判断是不是npm内容或者带有npm标识,若是是require('xxx') require('xxx/yyy')的形式获取package.json中的main部分找到引用文件,就去compile该文件(带上npm标识继续去resolveDeps),若是判断不是npm内容修正require便可,带有npm标识最后会打包到npm文件夹。
其余能够参考阅读的相关介绍文章
谢谢阅读~
欢迎follow我哈哈github.com/BUPT-HJM
欢迎继续观光个人新博客~(老博客近期可能迁移)
欢迎关注