开发小程序又一新选择 vue-mini,据说性能是 Taro 的 10 倍,遥遥领先

1. 前言

大家好,我是若川,欢迎关注我的公众号:若川视野。我倾力持续组织了 3 年多每周大家一起学习 200 行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(6k+人)第一的专栏,写有几十篇源码文章。

刚刚结束不久的vueconf 2024 深圳,有一个主题《Vue-Mini 不妥协的小程序框架》,仓库PPT视频

vueconf.cn 截图

PPT 中有这样两页。

vuemini-vs-taro.png

和 taro 性能对比数据的仓库链接,目前作者暂未给出与 uniapp 的性能对比。有小伙伴在 issue 中问到,作者回复后续会补上

vue-mini 官网 与其他的比较。

vue-mini 官网与其他的比较

vue-mini 官网与其他的比较2

更多兼容性和使用方法等查阅vue-mini 文档

本文主要来简单体验下 vue-mini,并且学习下基本的打包构建大概是如何实现的。

学完本文,你将学到:

1. vue-mini 初步体验
2. 初始化项目中的 build.js 是如何打包小程序代码的
3. 如何处理 ts、css、html 文件
4. 等等

2. 初始化项目

根据 官网文档快速开始 生成小程序项目,我采用的是 pnpm create vue-mini@latest,我都选择的”是”。如下图所示:

screenshot-cli.png

这个命令调用的是 create-vue-mini 这个项目,写文章时的版本是 1.0.4。它由 create-vue 修改而来。我在21年写过它的源码文章Vue 团队公开快如闪电的全新脚手架工具 create-vue,未来将替代 Vue-CLI,才300余行代码,学它!,(3.9w+阅读量、483赞)可供学习。

也可以直接克隆我的项目。

git clone https://github.com/ruochuan12/vue-mini-analysis.git
cd vue-mini-analysis
pnpm install

执行 pnpm install 之后,再执行 pnpm run dev 或者 pnpm run build 命令。

3. 体验 vue-mini

直接选择项目根目录而非 dist 目录,将此项目导入微信开发者工具。

打开项目如图:

vue-mini-project.png

vue-mini-project-mine.png

3.1 对比打包构建的代码

我们具体来对比执行 pnpm run dev 命令之后生成的代码。

入口 app.ts 文件
入口 app.ts 文件

首页 html 文件
首页 html 文件

首页 css 文件
首页 css 文件

首页 ts 文件
首页 ts 文件

首页 json 文件
首页 json 文件

我们可以看到主要就是处理入口 app.ts 文件(单独追加了 promise-polyfill)、htmlcssts文件编译成了微信小程序支持的 app.jswxmlwxssjsjson 文件是直接复制的,没做处理。

换句话说:
模板写法使用的是原生微信小程序的wxml,只是改名了 html 而已,css 部分也是原生微信小程序的 wxss 只是单位 rpx 改成了 px 而已,未做类似单文件组件的编译。只是在逻辑侧,ts 文件使用了 vue-mini/core,轻运行时,会把 ES Module 编译成 commonjs

3.2 dev 和 build 命令

pnpm run devpnpm run build 分别对应的是 package.json 中的两个命令。


{
 "scripts": {
  "dev": "cross-env NODE_ENV=development node build.js",
  "build": "cross-env NODE_ENV=production node build.js"
 }
}

cross-env 是用来跨平台设置环境变量的,NODE_ENV=development 代表开发环境,NODE_ENV=production 代表生产环境。

我们可以打开 build.js 文件,查看下它的代码。

build.png

调试可参考我的文章新手向:前端程序员必学基本技能——调试 JS 代码,或者据说 90%的人不知道可以用测试用例(Vitest)调试开源项目(Vue3) 源码。本文就不做过多赘述。

debugger.png

build.js 目前是比较原始的方式,没有各种封装,相对容易学习。有小伙伴提建议[Feature] 希望可以增强工程化等基建体验 #65

让我想起很久很久以前(大约是6年前),vue-cli@2.9.3 版本时就是用生成 vue 项目就是直接生成在开发者的项目中。比较难以和官方保持同步升级。后来 vue-cli@3.0 之后版本就能相对容易升级了。

当时写过一篇文章分析vue-cli@2.9.3 搭建的webpack项目工程,webpack 配置相关可能过时了,但其他知识没有过时,感兴趣的小伙伴可以学习。

3. build.js 打包构建文件

3.1 引入各种依赖


import path from 'node:path';
import process from 'node:process';

引入 node path 模块和 process 模块

import fs from 'fs-extra';
import chokidar from 'chokidar';

引入 fs-extra 模块,用来操作文件和目录
引入 chokidar 模块,用来监听文件变化

import babel from '@babel/core';
import traverse from '@babel/traverse';
import t from '@babel/types';
import { minify } from 'terser';

引入 @babel/core 模块,用来编译 js 代码
引入 @babel/traverse 模块,用来遍历 js 代码
引入 @babel/types 模块,用来编译 js 代码
引入 terser 模块,用来压缩 js 代码

import postcss from 'postcss';
import postcssrc from 'postcss-load-config';

引入 postcss 模块,用来编译 css 代码
引入 postcss-load-config 模块,用来加载 postcss 配置文件

import { rollup } from 'rollup';
import replace from '@rollup/plugin-replace';
import terser from '@rollup/plugin-terser';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

引入 rollup 模块,用来打包 js 代码
引入 @rollup/plugin-replace 模块,用来替换代码
引入 @rollup/plugin-terser 模块,用来压缩 js 代码
引入 @rollup/plugin-node-resolve 模块,用来解析 node_modules 中的依赖
引入 @rollup/commonjs 模块,用来解析 commonjs 依赖

import { green, bold } from 'kolorist';

引入 kolorist 模块,用来输出彩色文字

3.2 定义变量


let waitList = [];

const startTime = Date.now();

const NODE_ENV = process.env.NODE_ENV || 'production';

const __PROD__ = NODE_ENV === 'production';

const terserOptions = {
  ecma: 2016,
  toplevel: true,
  safari10: true,
  format: { comments: false },
};


const bundledModules = new Set();

3.3 调用 prod 或者 dev

if (__PROD__) {
  await prod();
} else {
  await dev();
}

我们先来看 prod 函数,再看 dev 函数。

3.4 prod 函数

async function prod() {
  await fs.remove('dist');
  const watcher = chokidar.watch(['src'], {
    ignored: ['**/.{gitkeep,DS_Store}'],
  });
  watcher.on('add', (filePath) => {
    const promise = cb(filePath);
    waitList.push(promise);
  });
  watcher.on('ready', async () => {
    const promise = watcher.close();
    waitList.push(promise);
    await Promise.all(waitList);
    console.log(bold(green(`构建完成,耗时:${Date.now() - startTime}ms`)));
  });
}

这个函数主要做了以下几件事:

  1. 移除 dist 目录
  2. 监听 src 目录
  3. 对于监听的文件,调用 cb 函数,把返回的 promise ,存入数组 waitList。
  4. 最后调用 Promise.all(waitList) 执行所有的 promise
  5. 最后打印构建时长。

3.5 dev 函数

async function dev() {
  await fs.remove('dist');
  chokidar
    .watch(['src'], {
      ignored: ['**/.{gitkeep,DS_Store}'],
    })
    .on('add', (filePath) => {
      const promise = cb(filePath);
      waitList?.push(promise);
    })
    .on('change', (filePath) => {
      cb(filePath);
    })
    .on('ready', async () => {
      await Promise.all(waitList);
      console.log(bold(green(`启动完成,耗时:${Date.now() - startTime}ms`)));
      console.log(bold(green('监听文件变化中...')));
      
      waitList = null;
    });
}

这个函数和 prod 函数类似,主要做了以下几件事:

  1. 移除 dist 目录
  2. 监听 src 目录
  3. 对于监听的文件,调用 cb 函数,把返回的 promise ,存入数组 waitList。
  4. 文件改变时,调用 cb 函数。
  5. 调用 Promise.all(waitList) 执行所有的 promise
  6. 最后打印启动时长,清空 waitList。

我们接着来看,cb 函数,这个函数用来处理文件变化。

const cb = async (filePath) => {
  if (/.ts$/.test(filePath)) {
    await processScript(filePath);
    return;
  }

  if (/.html$/.test(filePath)) {
    await processTemplate(filePath);
    return;
  }

  if (/.css$/.test(filePath)) {
    await processStyle(filePath);
    return;
  }

  await fs.copy(filePath, filePath.replace('src', 'dist'));
};

cb 函数主要用来处理 ts、html、css 文件和复制文件到 dist 目录。

分别来看这几个函数的实现,我们先看 processScript 处理 ts 文件

3.6 processScript 处理 ts

async function processScript(filePath) {
  let ast, code;
  try {
    const result = await babel.transformFileAsync(path.resolve(filePath), {
      ast: true,
    });
    ast = result.ast;
    code = result.code;
  } catch (error) {
    console.error(`Failed to compile ${filePath}`);

    if (__PROD__) throw error;

    console.error(error);
    return;
  }

使用 babel.transformFileAsync 异步地将文件内容转换为抽象语法树(AST)和转换后的代码。

  if (filePath.endsWith('app.ts')) {
    
    code = code.replace(
      '"use strict";',
      '"use strict";nnvar PromisePolyfill = require("promise-polyfill");nPromise = PromisePolyfill.default;',
    );
    const promise = bundleModule('promise-polyfill');
    waitList?.push(promise);
  }

替换代码 ‘”use strict”;’,追加 Promise 的 Polyfill,这里使用的是 promise-polyfill

  traverse.default(ast, {
    CallExpression({ node }) {
      if (
        node.callee.name !== 'require' ||
        !t.isStringLiteral(node.arguments[0]) ||
        node.arguments[0].value.startsWith('.')
      ) {
        return;
      }

      const promise = bundleModule(node.arguments[0].value);
      waitList?.push(promise);
    },
  });

遍历 AST,找到 CallExpression 节点,判断是否为 require 函数,并且参数是字符串,且不是相对路径。

  
  if (__PROD__) {
    code = (await minify(code, terserOptions)).code;
  }

  const destination = filePath.replace('src', 'dist').replace(/.ts$/, '.js');
  
  await fs.copy(filePath, destination);
  await fs.writeFile(destination, code);
}

经过以上处理后,src/pages/home/index.ts 变成了 dist/pages/home/index.js,代码如下所示:


import { defineComponent, ref } from '@vue-mini/core';

defineComponent(() => {
  const greeting = ref('欢迎使用 Vue Mini');

  return {
    greeting,
  };
});
"use strict";

var _core = require("@vue-mini/core");
(0, _core.defineComponent)(() => {
  const greeting = (0, _core.ref)('欢迎使用 Vue Mini');
  return {
    greeting
  };
});

我们来简单看下 babel 配置。

3.6.1 babel.config.js babel 配置文件

import fs from 'node:fs';

const runtimeVersion = JSON.parse(
  fs.readFileSync(
    new URL(import.meta.resolve('@babel/runtime/package.json')),
    'utf8',
  ),
).version;

const config = {
  targets: {},
  assumptions: {
    
  },
  presets: [
    [
      '@babel/preset-env',
      {
        bugfixes: true,
        modules: 'commonjs',
      },
    ],
    '@babel/preset-typescript',
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        version: runtimeVersion,
      },
    ],
    'transform-inline-environment-variables',
    [
      'module-resolver',
      {
        alias: {
          '@': './src',
        },
      },
    ],
    'autocomplete-index',
  ],
};

export default config;

我们继续来看 bundleModule 函数的具体实现。

3.7 bundleModule 打包模块

async function bundleModule(module) {
  if (bundledModules.has(module)) return;
  bundledModules.add(module);

  const bundle = await rollup({
    input: module,
    plugins: [
      commonjs(),
      replace({
        preventAssignment: true,
        values: {
          'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
        },
      }),
      resolve(),
      __PROD__ && terser(terserOptions),
    ].filter(Boolean),
  });
  await bundle.write({
    exports: 'named',
    file: `dist/miniprogram_npm/${module}/index.js`,
    format: 'cjs',
  });
}

如果已经有打包好的模块,直接返回。
rollup 打包模块,处理成 commonjs,并写入 dist/miniprogram_npm 目录。

如图所示:

npm.png

我们继续来看 html 文件处理:

3.8 processTemplate 处理模板 html

async function processTemplate(filePath) {
  const destination = filePath
    .replace('src', 'dist')
    .replace(/.html$/, '.wxml');
  await fs.copy(filePath, destination);
}

这个函数相对简单,就是复制 src html 文件修改后缀名为 .wxml 文件到 dist 目录。

3.9 processStyle 处理样式文件

async function processStyle(filePath) {
  
  const source = await fs.readFile(filePath, 'utf8');
  
  const { plugins, options } = await postcssrc({ from: undefined });

  let css;
  try {
    const result = await postcss(plugins).process(source, options);
    css = result.css;
  } catch (error) {
    console.error(`Failed to compile ${filePath}`);

    
    if (__PROD__) throw error;

    console.error(error);
    return;
  }

  const destination = filePath
    .replace('src', 'dist')
    .replace(/.css$/, '.wxss');
  
  await fs.copy(filePath, destination);
  await fs.writeFile(destination, css);
}

postcss-load-config Autoload Config for PostCSS 是自动加载 postcss.config.js 等配置文件,并解析其中的插件。
然后调用 postcss 解析样式文件,并写入 dist 目录。

3.9.1 postcss.config.js postcss 配置文件

import pxtorpx from 'postcss-pxtorpx-pro';

const config = {
  plugins: [pxtorpx({ transform: (x) => x })],
};

export default config;

引入 postcss-pxtorpx-pro 插件,将 px 转换为 rpx

处理 pxrpx 如下所示:

// input
h1 {
    margin: 0 0 20px;
    font-size: 32px;
    line-height: 1.2;
    letter-spacing: 1px;
}

// output
h1 {
  margin: 0 0 40rpx;
  font-size: 64rpx;
  line-height: 1.2;
  letter-spacing: 2rpx;
}

4. 总结

我们学习了初始化项目中的 build.js 是如何打包小程序代码的。

学习了使用 cross-env 配置环境变量,使用 chokidar 监听文件变动。html 文件就是原生微信小程序的wxml,直接复制粘贴修改了后缀名到dist目录。 使用 babelrollup 处理 js 文件,入口文件 app.config.ts ,还在开头追加了 promise-ployfill ,使用 postcss 处理样式文件,其他文件是直接复制粘贴到 dist 目录的。

也就是说:只是html(wxml)模板部分还是原生微信小程序写法,ts(js)逻辑部分使用了vue-mini(轻运行时)。

常言道:一图胜千言。我画了一张图表示:

vue-mini.drawio.png

vue-mini 比较适合不需要跨端,比如不需要同时支持微信小程序和支付宝小程序。只开发微信小程序是一个新选择,性能基本等于原生微信小程序,逻辑部分开发体验优于原生微信小程序。适合本身就是使用的原生微信小程序开发的,可以渐进式升级替换为 vue-mini

也就是说 vue-mini 是渐进式开发微信小程序。和原生开发不是二选一。性能上,vue-mini 接近原生,开发体验优于原生开发。

不过目前还处于相对初期阶段,生态还不是很完善,比如暂不支持 less、sass 等。

vue-mini 作者在最后也有一页接下来的开发方向的PPT。作者目前时间和精力有限,没有支持多端的打算。长期可能有。

vue-mini-next.png

感兴趣的小伙伴可以点个 star。我们持续关注后续发展,有余力的小伙伴也可以多参与贡献。


如果看完有收获,欢迎点赞、评论、分享、收藏支持。你的支持和肯定,是我写作的动力

作者:常以若川为名混迹于江湖。所知甚少,唯善学。若川的博客github blog,可以点个 star 鼓励下持续创作。

最后可以持续关注我@若川,欢迎关注我的公众号:若川视野。我倾力持续组织了 3 年多每周大家一起学习 200 行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(6k+人)第一的专栏,写有几十篇源码文章。

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=21327,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?