简化后的版本在这
这里也做了一点变更,uniapp
的实现中是如下形式,main.js
和页面.vue
会命中同一个loader
(见node_modules/@dcloudio/webpack-uni-mp-loader/lib/main-new.js
),然后在这个loader里面根据是是否有resouceQuery
来区别处理(显然main.js
没有,而页面.vue
有。
{
common/main: "/Users/songyu/tencent/doctor-uni/src/main.js",
pages/home/main: "/Users/songyu/tencent/doctor-uni/src/main.js{\"page\":\"pages%2Fhome%2Fmain\"}"
}
现在调整为如下形式,这样逻辑会更清晰些:
{
entry: {
'common/main': '/Users/songyu/songyu/mock-uniapp-for-wxmp/demo/src/main.js',
'pages/home/main': '/Users/songyu/songyu/mock-uniapp-for-wxmp/demo/src/pages/home/main.vue',
'pages/sub/sell/index': '/Users/songyu/songyu/mock-uniapp-for-wxmp/demo/src/pages/sub/sell/index.vue'
},
}
实际上原始的做法是有好处的,由于entry
性质再加上利用中间代码的逻辑可以实现页面组件自动注册
// node_modules/@dcloudio/webpack-uni-mp-loader/lib/main-new.js
module.exports = function (content, map) {
if(this.resouceQuery){
// ...
const params = loaderUtils.parseQuery(this.resourceQuery)
if (params && params.page) {
params.page = decodeURIComponent(params.page)
return this.callback(null,
`import Vue from 'vue'
import Page from './${normalizePath(params.page)}${ext}'
createPage(Page)`, map)
}
}
else {
//...
}
}
下面展开说一说entry带来的模块的自执行特性和中间代码实现页面自动注册的逻辑
关于自动注册:执行createPage
-> Page
,createPage
方法是uniapp
运行时中用来连接vue
和小程序
的「桥」的一部分,这个非常重要,后面我们细说。
entry
在这里重要作用之一是:在webpack
中,每个entry
至少会生成一个chunk
,而entry
指向的模块是需要被执行的。我们知道webpack
提供了自己的模块化机制,也就是构建后的runtime.js,其中有一个关键的全局方法webpackJsonpCallback
,该方法接口一个数组,数组的第三项的含义是executeModules
,即需要执行的模块以及该模块的依赖。
以我们当前项目产物中的common/main.js
为例,该文件(chunk)是由entry
中的common/main
最终输出的。看下其结构,注意[0,"common/runtime","common/vendor"]
部分,0
是一个moduleId有webpack自己生成,指向了一个模块,在这里是src/main.js
,而"common/runtime"
,"common/vendor"
则是该模块的依赖,即需要执行完后两个模块后,才能执行src/main.js
。
// dist/common/main.js
(global["webpackJsonp"] = global["webpackJsonp"] || []).push([["common/main"],[
// 这个中间数组存储着当前chunk中所有模块的定义
],[[0,"common/runtime","common/vendor"]]]);
为什么说这个特性是有entry
带来的呢,首先说明这显然是合理的,因为entry
执行的文件就是被认为是起始节点,那起始节点的触发当然是自触发啦,因此webpack就会通过这种方式来自动执行entry
指向的模块。而在webpack
源码中也可以明确看到这部分逻辑,如下:,重点关注chunk.entryModule
,每个entry
指向的文件会创建一个模块,也会创建一个chunk
,chunk
包含该模块,并且该模块是该chunk
的入口,后续该chunk
包含的所有模块都是该入口模块依赖链上关联的模块。
// node_modules/webpack/lib/web/JsonpChunkTemplatePlugin.js
const getEntryInfo = chunk => {
return [chunk.entryModule].filter(Boolean).map(m =>
[m.id].concat(
Array.from(chunk.groupsIterable)[0]
.chunks.filter(c => c !== chunk)
.map(c => c.id)
)
);
};
class JsonpChunkTemplatePlugin {
apply(chunkTemplate) {
chunkTemplate.hooks.render.tap('JsonpChunkTemplatePlugin', (modules, chunk) =>{
//...
source.add(modules); // 模块定义
const entries = getEntryInfo(chunk);
if (entries.length > 0) {
source.add(`,${JSON.stringify(entries)}`); // executeModules 模块的添加
}
//...
})
}
}
上面解释了一大堆只是解释了自执行这一点,下面看看上述main-new.js
返回的中间代码的作用。
看到返回了中间代码
// 中间代码返回给webpack,webpack会将从执行parser.parse解析这段代码并收集依赖,然后继续构建这些依赖
import Vue from 'vue'
import Page from './pages/home/main.vue' // 具体的例子更容易理解
createPage(Page)
你知道import Page from './pages/home/main.vue'
返回的是什么吗?从如果你看过运行createPage
方法的逻辑应该猜得出,是<script/>
返回的组件选项。也就是类似如下形式
{
data () {
return {...}
},
components: {...}
}
然后createPage
接受这些选项后会调用小程序框架提供的Page
构造函数来进行小程序页面的注册,而后在attached生命周期中再去创建Vue
组件实例,并将vue实例
和小程序实例
进行关联,这部分在后面介绍运行时会再深入分析到。
结合上面两点,可以总结出当前entry
生成的文件(webpack
代码中的概念是chunk
),会自动执行createPage(vueOptions)
这样的逻辑来实现小程序页面的自动注册,这两者少一个都不行,如果最终没有自动调用Page(optins)
,开发者工具的报错是组件找不到,最初是碰到这样的错误时,我也是一脸懵,这个页面/组件不是有吗,后面排查后发现原来是没有成功注册
。
uniapp中的vue文件实际上可以分为三大类:
都是vue文件,构建过程中如何区分呢,App.vue是固定写法,通过文件名称就可以判断,页面一定是来自app.json(uniapp中叫pages.json)配置,所有判断页面路径是不是在配置中,如果是则是页面,如果不是则认为是组件。
上面说到页面.vue
是作为entry
的,因此会自动执行然后结合中间代码createPage(options)
实现页面的注册。
那组件呢?组件并不会作为entry而被构建,组件在uniapp中是作为全局组件或者页面依赖的组件而被动态发现和构建的。
由于不是作为entry进行构建,因此失去了自动执行的能力,也就是uniapp构建完后的组件的js文件中没有executeModules
,如下
(global["webpackJsonp"] = global["webpackJsonp"] || []).push([["components/todo-item"],{
//... 模块定义
}, /*这里没有 executeModules 哦*/]);
并且uniapp当前的实现中也没有通过中间代码的形式来添加进行组件注册逻辑即createComponent(options)
我们先看下官方是如何做的
// node_modules/@dcloudio/webpack-uni-mp-loader/lib/plugin/index-new.js
class WebpackUniMPPlugin {
apply (compiler) {
compiler.hooks.emit.tapPromise('webpack-uni-mp-emit', compilation => {
return new Promise((resolve, reject) => {
//...
// node_modules/@dcloudio/webpack-uni-mp-loader/lib/plugin/generate-component.js
generateComponent(compilation, compiler.options.output.jsonpFunction);
resolve();
});
});
}
}
上面的generateComponent
方法主要就是做这件事情,即添加自动执行组件模块和组件注册的逻辑,具体的过程这里就不分析了。看下组件的构建产物就可以直观的知道了。会被自动添加如下逻辑。实际上这是手动创建了一个【chunk】啊,哈哈。
(global["webpackJsonp"] = global["webpackJsonp"] || []).push([["components/todo-item"],{
//... 模块定义
}, /*这里没有 executeModules 哦*/]);
// 下面部分才是 generateComponent的成果
;(global["webpackJsonp"] = global["webpackJsonp"] || []).push([
'components/todo-item-create-component',
{
'components/todo-item-create-component':(function(module, exports, __webpack_require__){
__webpack_require__('1')['createComponent'](__webpack_require__(22))
})
},
[['components/todo-item-create-component']]
]);
当该js文件被加载后,就会执行__webpack_require__('1')['createComponent'](__webpack_require__(22))
这段逻辑,在这个具体的例子里__webpack_require__('1')
是运行时之一,是uniapp源码中的node_modules/@dcloudio/uni-mp-weixin/dist/index.js
这个文件,会返回具备【桥】能力的api如createPage、createApp、createComponent等。而__webpack_require__(22)
则是当前组件的选项,即vue文件script部分导出的内容。这样就自动执行和注册组件的能力。
页面.vue
的情况实际上是不用处理的,因为entry指向的文件会自动被执行。因此我们要考虑如何优雅的自动执行组件(js)呢。上面其实已经分析了,只需要将moduleId
作为executeModules
传递就好,那就需要给这个chunk设置手动设置一个entryModule
,那么在JsonpChunkTemplatePlugin
生成代码时会自动拼接上。为此,我实现下面插件
module.exports = class {
apply(compiler) {
compiler.hooks.thisCompilation.tap('a', compilation => {
compilation.hooks.chunkAsset.tap('b', function (file) {
const module = [...file.modulesIterable].find(module => /\.vue$/.test(module.userRequest));
if (!module) return;
const chunks = [...module.chunksIterable]
const chunk = chunks && chunks[0]
if (!chunk) return;
if (!chunk.entryModule) { // 页面vue是entry,因此自然有,组件vue不是,刚好在这加
// 这样做的目的是可以自动执行该模块
// 见 lib/web/JsonpChunkTemplatePlugin.js -> getEntryInfo
chunk.entryModule = module;
}
})
})
}
}
逻辑很简单,就是在一个合适的时机(我这里是在hooks.chunkAsset时),给组件所在的chunk设置entryModule
。
(global["webpackJsonp"] = global["webpackJsonp"] || []).push([["components/todo-item"],{
// 模块定义
},[[23]]]); // [23]就是该插件作用的结果
这里另外需要关注的点是,组件确实是会创建一个新的独立的chunk。这是因为小程序框架是这么要求的,每个组件都是独立的,即wxss,wxml,js,json都需要有。因此组件的这些内容都需要被拆分出来。
具体是怎么做的,后面在介绍组件构建时,会再分析到。
我目前的做法是稍稍动了下vue-loader,感觉你有必要了解vue-laoder的工作原理,可以参考我之前的文章:「.vue文件的编译」1. vue-loader@15.8.3 的整体流程
// node_modules/vue-loader/lib/loaders/pitcher.js
module.exports = code => code
module.exports.pitch = function (remainingRequest) {
if(query.type === 'style'){
//...
// return ...
}
if(query.type === 'template'){
//...
// return ...
}
if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {
return ``
}
// 进入到后面逻辑的事:type=script
//... 修改 看下面截图吧
}
命中vue&type=xxx
这个resourceQuery
的模块会走这里,我们修改了type=script
的情况,
逻辑很简单区分了vue文件的组件类型(App,页面还是组件),而后注入不同的逻辑。你需要知道上面的mod
是<script/>
部分返回的vue组件选项。然后createPage(mod)
或者createComponent(mod)
即可。这样当组件js被执行时,自然也会执行这段逻辑,就ok了。你可以看下我提供的案例中的组件的构建结果。否则可能有点懵。
TODO
看起来文章是有点干,大家都喜欢看各种图,因为就不用思考,清晰明了,看起来高端。文章主要还是自己的记录,如果后面时间允许,能够出个小册之类的,可能会有更多的心思尽量让文章更容易理解些。