1. 前言
大家好,我是若川,欢迎关注我的公众号:若川视野。我倾力持续组织了 3 年多每周大家一起学习 200 行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02
参与。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(6k+人)第一的专栏,写有几十篇源码文章。
刚刚结束不久的vueconf 2024 深圳,有一个主题《Vue-Mini 不妥协的小程序框架》,仓库、PPT、视频
PPT 中有这样两页。
和 taro 性能对比数据的仓库链接,目前作者暂未给出与 uniapp
的性能对比。有小伙伴在 issue 中问到,作者回复后续会补上
vue-mini 官网 与其他的比较。
更多兼容性和使用方法等查阅vue-mini 文档。
本文主要来简单体验下 vue-mini
,并且学习下基本的打包构建大概是如何实现的。
学完本文,你将学到:
1. vue-mini 初步体验
2. 初始化项目中的 build.js 是如何打包小程序代码的
3. 如何处理 ts、css、html 文件
4. 等等
2. 初始化项目
根据 官网文档快速开始 生成小程序项目,我采用的是 pnpm create vue-mini@latest
,我都选择的”是”。如下图所示:
这个命令调用的是 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
目录,将此项目导入微信开发者工具。
打开项目如图:
3.1 对比打包构建的代码
我们具体来对比执行 pnpm run dev
命令之后生成的代码。
入口 app.ts 文件
首页 html 文件
首页 css 文件
首页 ts 文件
首页 json 文件
我们可以看到主要就是处理入口 app.ts
文件(单独追加了 promise-polyfill
)、html
、css
、ts
文件编译成了微信小程序支持的 app.js
、wxml
、wxss
、js
。json
文件是直接复制的,没做处理。
换句话说:
模板写法使用的是原生微信小程序的wxml
,只是改名了 html
而已,css
部分也是原生微信小程序的 wxss
只是单位 rpx
改成了 px
而已,未做类似单文件组件的编译。只是在逻辑侧,ts
文件使用了 vue-mini/core
,轻运行时,会把 ES Module
编译成 commonjs
。
3.2 dev 和 build 命令
pnpm run dev
和 pnpm 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
文件,查看下它的代码。
调试可参考我的文章新手向:前端程序员必学基本技能——调试 JS 代码,或者据说 90%的人不知道可以用测试用例(Vitest)调试开源项目(Vue3) 源码。本文就不做过多赘述。
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';
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`)));
});
}
这个函数主要做了以下几件事:
- 移除 dist 目录
- 监听 src 目录
- 对于监听的文件,调用 cb 函数,把返回的 promise ,存入数组 waitList。
- 最后调用 Promise.all(waitList) 执行所有的
promise
。 - 最后打印构建时长。
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 函数类似,主要做了以下几件事:
- 移除 dist 目录
- 监听 src 目录
- 对于监听的文件,调用 cb 函数,把返回的 promise ,存入数组 waitList。
- 文件改变时,调用 cb 函数。
- 调用 Promise.all(waitList) 执行所有的
promise
。 - 最后打印启动时长,清空 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
目录。
如图所示:
我们继续来看 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
。
处理 px
为 rpx
如下所示:
// 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
目录。 使用 babel
和 rollup
处理 js
文件,入口文件 app.config.ts ,还在开头追加了 promise-ployfill
,使用 postcss
处理样式文件,其他文件是直接复制粘贴到 dist
目录的。
也就是说:只是html
(wxml
)模板部分还是原生微信小程序写法,ts
(js
)逻辑部分使用了vue-mini
(轻运行时)。
常言道:一图胜千言。我画了一张图表示:
vue-mini
比较适合不需要跨端,比如不需要同时支持微信小程序和支付宝小程序。只开发微信小程序是一个新选择,性能基本等于原生微信小程序,逻辑部分开发体验优于原生微信小程序。适合本身就是使用的原生微信小程序开发的,可以渐进式升级替换为 vue-mini
。
也就是说 vue-mini
是渐进式开发微信小程序。和原生开发不是二选一。性能上,vue-mini
接近原生,开发体验优于原生开发。
不过目前还处于相对初期阶段,生态还不是很完善,比如暂不支持 less、sass
等。
vue-mini
作者在最后也有一页接下来的开发方向的PPT。作者目前时间和精力有限,没有支持多端的打算。长期可能有。
感兴趣的小伙伴可以点个 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