3. 「uniapp 如何支持微信小程序环境开发」配置项简化到可以让你一盔全貌之:loader + plugin

2023-03-10 14:24:32 浏览数 (2)

本节我们看下module.rules plugin相关的配置


.vue文件的编译

当然要配vue-loader啊,.vue文件解析全靠他了。vue-loader的整体流程的分析可以参考我之前的文章:「.vue文件的编译」1. vue-loader@15.8.3 的整体流程

下面是我们当前简易demo中的vue-loader的配置

代码语言:javascript复制
{
    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.compileroptions.compilerOptions

代码语言:javascript复制
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

代码语言:javascript复制
// 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.vuetempalte如下

代码语言:javascript复制
<template>
  <div class="global-compo">  
    全局组件: {{ name }} <br />
    属性值: {{ content }}
  </div>
</template>

构建后会生成单独的wxml文件

代码语言:javascript复制
<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插件

代码语言:javascript复制
// loader配置
{
    test: /.css$/i,
    use: [MiniCssExtractPlugin.loader, "css-loader"],
},

// plugin
new MiniCssExtractPlugin({filename: "[name].wxss"}),

当然如果你想支持less/sass等,需要额外增加less-loadersass-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文件中解析出依赖的组件,需要注意的是解析全局组件和局部组件是两种不同的解析方式,这是由使用方式决定的。当前简化后版本实现中,两种类型组件的注册限定为下面方式:

代码语言:javascript复制
// 全局组件注册方式如下:
// 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&amp;type=script/,
    use: [
        {
            loader: path.resolve(__dirname, 'mp/script.js')
        }
    ]
},

看到上面第一个loader提供了一个选项,这个会在本节后面会分析到,不要着急哈。


细节:如果你只是做上面的转化会发现构建的代码运行会报错: 因为你使用了document.create('script'),这是由于动态加载js就是这么做的。而实际上,小程序组件的加载是框架加载的,因为我们将上述动态加载的逻辑放在一个不会执行的函数中,就好了。不如

代码语言:javascript复制
var todoItem = function(){
    import(.....)
}

显然这里应该 -> .json文件

我这里把小程序里面的.json文件暂时分为三类

  1. app.json
  2. 页面或者组件的json
  3. 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文件构建成小程序组件的形式,但是显然最终的运行怎么能离开运行时代码呢?

运行时实际上包括两个

  1. vue运行时,响应式,自定义事件等逻辑,还是来自vue提供的特性。
  2. 桥:连接 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 &amp;&amp; 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

代码语言:javascript复制
resolve: {
    alias: {
        "vue$": path.resolve(__dirname, "../runtime/mp-vue/mp.runtime.esm.js")
    }
},

因为bridge这个运行时暴露了将createAppcreatePagecreateComponent直接挂载到了wx这个全局对象上,并且我对他们也是直接这么使用的即 wx.createXxx。需要知道运行时是整个框架最底层的依赖,必须是最前置的,当然webpack生成的runtime实际上是模块化机制,当然更加底层了,所以app.js得第一行就是require('./common/runtime.js')

但是bridge运行时还暴露很多uni上的api,如uni.showToast等,这些是模块内的。因此做法是通过ProvidePlugin来自动导入依赖的变量

代码语言:javascript复制
new webpack.ProvidePlugin({
    createApp:       [mpRuntime, 'createApp'],
    createComponent: [mpRuntime, 'createComponent'],
    createPage:      [mpRuntime, 'createPage'],
    uni:             [mpRuntime, 'default']
})

总结

  1. 重点是 vue 文件如何转换为小程序组件结构
  1. 配置文件 app.json主要来自开发者自己的配置,其中的usingComponents会在解析src/main.jssrc/App.vue文件时收集到全局组件并最终更新到app.json中。
  2. 运行时相关:【vue】运行时和【桥】运行时,我的做法是直接作为底层依赖,首先被加载。(uniapp不是这么做的)

0 人点赞