本节我们看下module.rules
plugin
相关的配置
.vue
文件的编译
当然要配vue-loader
啊,.vue
文件解析全靠他了。vue-loader
的整体流程的分析可以参考我之前的文章:「.vue文件的编译」1. vue-loader@15.8.3 的整体流程
下面是我们当前简易demo中的vue-loader
的配置
{
test: /.vue$/,
use: [
{
loader: 'vue-loader',
options: {
compiler: require('./uni-template-compiler'),
compilerOptions: { // 这里你可以先忽略,后面会单独小节分析
mp: {
platform: "mp-weixin",
scopedSlotsCompiler: "auto",
},
filterModules: {},
filterTagName: 'wxs',
},
}
},
]
},
const { VueLoaderPlugin } = require('vue-loader')
new VueLoaderPlugin(),
uniapp实际上也对其进行了改造,并放到@dcloudio/vue-cli-plugin-uni/packages
路径下,当然模板编译的核心库vue-template-compiler
。也做了部分更改。
对比后发现是针对部分特性如easycom(auto-components
),renderjs,custom-blocks如<wxs>
的支持。这些都不是关键的特性,属于优化层面的,这些特性实际上可以丢掉的。所以这里我是使用原始的vue-loader
。
<template>
块拆分为 -> .wxml
文件
上面的配置中有个关键信息,即提供了 options.compiler
和options.compilerOptions
options: {
compiler: require('./uni-template-compiler'),
compilerOptions: {
mp: {
platform: "mp-weixin",
scopedSlotsCompiler: "auto",
},
filterModules: {},
filterTagName: 'wxs',
},
}
.vue
文件中有一个<template>
块,这个块的最终编译实际上是由templateLoader
转交给vue-template-compiler
处理的,vue-template-compiler
是用来将模板编译成render
函数,render函数返回一个虚拟DOM树。vue-template-compiler
虽然是一个单独的库,但实际上是由vue源码中的 src/compiler
部分构成,每次发布vue版本时对应发布这个库,一对一的,用来生成当前vue版本需要的虚拟DOM树。实际上vue也提供runtime compiler版本,即可以线上动态编译template
。
// node_modules/vue-loader/lib/loaders/templateLoader.js
module.exports = function (source) {
//...
const compiler = options.compiler || require('vue-template-compiler')
const compilerOptions = Object.assign({...}, options.compilerOptions, {...})
// for vue-component-compiler
const finalOptions = {
source,
filename: this.resourcePath,
compiler,
compilerOptions, ...
}
//...
const compiled = compileTemplate(finalOptions)
//...
const { code } = compiled
// finish with ESM exports
return code `nexport { render, staticRenderFns }`
}
看到了吗,上面的compiler的来源,如果有设置options.compiler
则用配置提供的,如果没有则默认使用vue-template-compiler
,并且也会将我们提供的compilerOptions
合并到最终的选项中,传递给构建方法compileTemplte
。 因为默认的vue-template-compiler
只会讲我们的<template>
编译成render函数,但是小程序要的确是真真切切的wxml
文件,所以啊,提供了自己的模板编译,用来生成wxml文件。
比如我们demo中的src/components/global-com.vue
的tempalte
如下
<template>
<div class="global-compo">
全局组件: {{ name }} <br />
属性值: {{ content }}
</div>
</template>
构建后会生成单独的wxml
文件
<view class="global-compo _div">
{{'全局组件: ' name ''}}
<view class="_br"></view>
{{'属性值: ' content ''}}
</view>
关于模板编译这里实际上我感觉可能是整个uniapp最难的地方(在我看来是这样),这部分我后面会单独小节分析,uniapp是如何做的,以及我自己的实现思路。
先简单说下:uniapp在这里实际上是将vue-template-compiler
返回后的render
函数转换成AST,而后根据每个节点的情况去处理成小程序wxml
要求的规范。然后通过loaderContext.emitFile(..)
来生成文件的
<style>
块拆分为 -> .wxss
文件
.wxss
是小程序组件的样式文件。
没什么好说的:css-loader
mini-css-extract-plugin
插件
// loader配置
{
test: /.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
// plugin
new MiniCssExtractPlugin({filename: "[name].wxss"}),
当然如果你想支持less/sass等,需要额外增加less-loader
,sass-loader
等。
<script>
拆分为 -> .js
文件
webpack
中对于模块的拆分,首先想到的就是SplitChunksPlugin,否则默认情况下,同步引用
的模块
是不会被拆分出去的。
我想使用该插件应该也能达到目的,只要你把cacheGroups
规则配置好。
但是有更好的方式使用 require.ensure、import() 方式来引用模块,这两种方式都是动态加载模块的方式,webpack
会自动将相应的模块拆分出去作为单独的chunk
,原理见我之前的系列文章「webpack源码分析」从dependency graph 到 chunk graph。
那具体该如何做呢,毕竟开发的时候是import xxxComponent from 'xxxComponent'
。
看下具体的例子吧
代码语言:javascript复制// 我们demo中的 src/main.js 文件 注册的全局组件为例
import globalCompo from './components/global-compo.vue'
Vue.component('global-compo', globalCompo);
转换成下面
代码语言:javascript复制import('./components/todo-item.vue' /* webpackChunkName: "components/todo-item" */)
注意:注释里面的webpackChunkName
非常重要,webpack称为magic comments,猜得出会将拆分出的新chunk
命名为此或者放到这个路径下?自己感受吧
或者转化成下面
代码语言:javascript复制require.ensure([], () => resolve(require('./components/todo-item.vue')), 'components/todo-item')
显然我们需要在loader阶段做,解析依赖之前(parser.parse
)。原因见之前的系列文章webpack源码分析」模块构建之解析_source获取dependencies
所以就有了这样的一个loader,来将.vue
文件中引用的组件形式进行替换。两个大的步骤
先是要从.vue
文件中解析出依赖的组件,需要注意的是解析全局组件和局部组件是两种不同的解析方式,这是由使用方式决定的。当前简化后版本实现中,两种类型组件的注册限定为下面方式:
// 全局组件注册方式如下:
// src/main.js
Vue.component()
// src/App.vue
export default {
components: {}
}
// 局部组件注册方式如下
// .vue文件
export default {
components: {}
}
然后替换组件的引用方式。
loader实现见; 全局组件解析、 局部组件解析loader
配置如下:
代码语言:javascript复制{
test: `${context}/main.js`,
use: [
{
loader: path.resolve(__dirname, 'mp/main.js'),
options: {
bridge: path.resolve(__dirname, '../../runtime/uni-mp-weixin/index')
},
},
]
},
{
resourceQuery: /vue&type=script/,
use: [
{
loader: path.resolve(__dirname, 'mp/script.js')
}
]
},
看到上面第一个loader提供了一个选项,这个会在本节后面会分析到,不要着急哈。
细节:如果你只是做上面的转化会发现构建的代码运行会报错: 因为你使用了document.create('script')
,这是由于动态加载js就是这么做的。而实际上,小程序组件的加载是框架加载的,因为我们将上述动态加载的逻辑放在一个不会执行的函数中,就好了。不如
var todoItem = function(){
import(.....)
}
显然这里应该 -> .json
文件
我这里把小程序里面的.json
文件暂时分为三类
- app.json
- 页面或者组件的json
- project.config.json、sitemap.json 等配置文件
上面第三种在当前实现中没有做特殊处理,就是直接复制,因此引入了copy-webpack-plugin
插件
当前的设计是(借鉴uniapp):src/下有一个app.json(uniapp是pages.json),然后和小程序官方配置的区别是,页面路径的配置,官方是字符串数组,我们这里改为对象数组,目的是为了设置各页面的style等配置。如下:
app.json和各页面组件的json文件基于该文件生成,在构建过程中的唯一变化是会修改usingComponents
这个是在上一部分解析组件引用的情况时会保存下来。 最后实现了一个插件来输出这些.json
文件
逻辑比较简单,不深入分析了,代码在这mp-plugin
全局文件 app.js、app.wxss
这两个文件是手动生成的,调用compilation.assets
就行
插件代码在这中的generateApp
方法
逻辑比较简单,不分析了。
运行时
到目前为止一直在说构建的问题,如何把.vue文件构建成小程序组件的形式,但是显然最终的运行怎么能离开运行时代码呢?
运行时实际上包括两个
- vue运行时,响应式,自定义事件等逻辑,还是来自vue提供的特性。
- 桥:连接 vue运行时和小程序框架
运行时代码的分析还是会单独小节分析,这里只是说如何将运行时模块注入到最终的产物中。
实际上处理也很简单,利用中间代码来加入额外的模块依赖,见
代码语言:javascript复制const loaderUtils = require('loader-utils')
const processComponents = require('./babel/index');
// 暂时只解析 Vue.component('', todoItem) 写法
const traverse = require('./babel/global-component-traverse');
module.exports = async function (content) {
this.cacheable && this.cacheable();
if (this.resourceQuery) {
//... 分析组件依赖,替换组件为动态导入,更新json.usingComponents
} else {
const options = loaderUtils.getOptions(this);
// 注意通过?main让 src/main.js下次被解析是走上面的if分支
return [`import Vue from 'vue'`, `import '${options.bridge}'`, `import '${this.resourcePath}?main'`].join('n');
}
}
options.bridge
在上面提到过,文件的本地连接。import Vue from 'vue'
结合resolve.alias
resolve: {
alias: {
"vue$": path.resolve(__dirname, "../runtime/mp-vue/mp.runtime.esm.js")
}
},
因为bridge这个运行时暴露了将createApp
、createPage
、createComponent
直接挂载到了wx
这个全局对象上,并且我对他们也是直接这么使用的即 wx.createXxx
。需要知道运行时是整个框架最底层的依赖,必须是最前置的,当然webpack生成的runtime实际上是模块化机制,当然更加底层了,所以app.js得第一行就是require('./common/runtime.js')
但是bridge运行时还暴露很多uni上的api,如uni.showToast
等,这些是模块内的。因此做法是通过ProvidePlugin来自动导入依赖的变量
new webpack.ProvidePlugin({
createApp: [mpRuntime, 'createApp'],
createComponent: [mpRuntime, 'createComponent'],
createPage: [mpRuntime, 'createPage'],
uni: [mpRuntime, 'default']
})
总结
- 重点是 vue 文件如何转换为小程序组件结构
- 配置文件
app.json
主要来自开发者自己的配置,其中的usingComponents
会在解析src/main.js
和src/App.vue
文件时收集到全局组件并最终更新到app.json中。 - 运行时相关:【vue】运行时和【桥】运行时,我的做法是直接作为底层依赖,首先被加载。(uniapp不是这么做的)