uniapp 是一个使用 vue.js 开发前端应用的框架,实现一套代码多端运行的效果,由 DCloud 团队开发维护。基于上半年都使用了 uniapp 开发小程序,这次来探讨下小程序的跨平台方案是怎么实现的
基础知识
node 包管理基础
npm 是 node 包管理工具,项目根目录下的 package.json 是 npm 的配置文件,且包都存放在 node_modules 下,结构如下图:
.|-- dist|-- public| `-- index.html|-- node_modules| |-- .bin| | `-- vue-cli-service| `-- vue|-- src| |-- platforms| |-- `-- mp-weixin| |-- pages| |-- wxcomponents| |-- App.vue| |-- main.js| |-- manifest.json| |-- pages.json| `-- uni.scss|-- test| `-- usTimezone.test.js|-- README.md|-- package.json`-- vue.config.js
其中每个项目都会有独立的拥有一份依赖,运行 package.scripts 里面的命令则运行脚本
{"name": "weniuprj","version": "0.1.0","private": true,"bin": {"vue-cli-service": "bin/vue-cli-service.js"},"scripts": {"serve": "npm run dev:mp-weixin","build": "npm run build:mp-weixin","lint": "vue-cli-service lint","build:mp-qq": "cross-env NODE_ENV=production UNI_PLATFORM=mp-qq vue-cli-service uni-build","build:mp-toutiao": "cross-env NODE_ENV=production UNI_PLATFORM=mp-toutiao vue-cli-service uni-build","build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build",// ..."dev:mp-qq": "cross-env NODE_ENV=development UNI_PLATFORM=mp-qq vue-cli-service uni-build --watch","dev:mp-toutiao": "cross-env NODE_ENV=development UNI_PLATFORM=mp-toutiao vue-cli-service uni-build --watch","dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch","info": "node node_modules/@dcloudio/vue-cli-plugin-uni/commands/info.js","test:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin jest -i"},"dependencies": {},"devDependencies": {}}
webpack 基本介绍
webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

核心概念:
- 入口(entry)
- 输出(output)
- loader
- 插件(plugin)
- 浏览器兼容(brower compatibility)
Uniapp
命令
通过执行不同的构建命令实现设置环境变量,实现配置和产物的不同
cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-buildcross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch
vue-cli
vue 框架的脚手架,提供 修改 webpack 配置、vue-cli-service 命令、创建项目等功能
执行 vue-cli-service 命令时,会自动加载 package.json 下所有名字为 vue-cli-plugin-<name> 的 server 插件,即该 npm 包下的 index.js,通过 server 插件去修改 webpack 配置、添加或修改 cli-service 命令,上面的 uni-build 命令就是通过改方法实现
// packages/vue-cli-plugin-uni/index.jsconst initBuildCommand = require('./commands/build')const initServeCommand = require('./commands/serve')module.exports = (api, options) => {initServeCommand(api, options)// 注册 uni-build 命令initBuildCommand(api, options)// ...api.configureWebpack(require('./lib/configure-webpack')(platformOptions, manifestPlatformOptions, options, api))api.chainWebpack(require('./lib/chain-webpack')(platformOptions, options, api))// ...}// packages/vue-cli-plugin-uni/commands/build.jsmodule.exports = (api, options) => {api.registerCommand('uni-build', {description: 'build for production',usage: 'vue-cli-service uni-build [options]',options: {'--watch': 'watch for changes','--minimize': 'Tell webpack to minimize the bundle using the TerserPlugin.','--auto-host': 'specify automator host','--auto-port': 'specify automator port','--subpackage': 'specify subpackage','--plugin': 'specify plugin','--manifest': 'build manifest.json'}}, async (args) => {// 命令执行内容,即打包小程序});};
构建入口
将通过不同命令,执行不同入口
// @dcloudio/vue-cli-plugin-uni/lib/mp.jsmodule.exports = {webpackConfig () {return {entry () {return process.UNI_ENTRY},// ... 其他配置module: {rules: [{// 自定义 main.js 解析 loadertest: path.resolve(process.env.UNI_INPUT_DIR, getMainEntry()),use: [{loader: '@dcloudio/webpack-uni-mp-loader/lib/main'}]}],},};}}

配置文件解析
manifest.json
用来生成小程序配置文件 project.config.json ,uniapp 通过 webpack-uni-pages-loader 去读取对应 platforms 下配置参数兼容不同的小程序
pages.json
uniapp 所有页面配置都在 pages.json 里面,构建入口都是 src/main.js 文件,通过不同 webpack-uni-mp-loader 去实现不同页面的构建
// packages/webpack-uni-pages-loader/lib/index.jsmodule.exports = function (content, map) {// 获取 mainfest.json 和 pages 路径const manifestJsonPath = path.resolve(process.env.UNI_INPUT_DIR, 'manifest.json')const pagesJsonJsPath = path.resolve(process.env.UNI_INPUT_DIR, pagesJsonJsFileName)const manifestJson = parseManifestJson(fs.readFileSync(manifestJsonPath, 'utf8'))this.addDependency(manifestJsonPath)this.addDependency(pagesJsonJsPath)const pagesJson = parsePagesJson()// ... 其余代码// 各平台代码默认配置兼容const jsonFiles = require('./platforms/' + process.env.UNI_PLATFORM)(pagesJson, manifestJson)// 输出 jsonchangedEmitFiles.forEach(name => {this.emitFile(name + '.json', emitFileCaches[name])})}
条件编译
通过使用 ifdef … endif 将不同代码打包到不同平台,使各端代码无冗余代码,如以下代码:
// src/pages/index/indeximport units, { showTip } from '../../utils';export default {data() {return {title: 'Hello',};},onLoad() {uni.showToast({ title: 'test' });// #ifdef MP-WEIXINsetTimeout(() => units.showTip('一秒延时'), 1000);// #endif// #ifdef MP-ALIPAYsetTimeout(() => showTip('两秒延时'), 2000);// #endif},methods: {},};
实现逻辑为 webpack 的一个 loader 插件,将所有文件内容都通过配置的正则规则将条件编译代码去除,代码会依次由:
- main.js
- app.vue#JavaScript
- pages.vue#javaScript
- app.vue#html
- pages.vue#html
- app.vue#css
- pages.vue#css
```javascript
// packages/vue-cli-plugin-uni/packages/webpack-preprocess-loader/index.js
function() {
try {
content = preprocessor.preprocess(content, context, {
}) } catch (e) {type
} }console.error('异常提醒')
// packages/vue-cli-plugin-uni/packages/webpack-preprocess-loader/preprocess/lib/regexrules.js const regexrules = { html: { if: { start: ‘[ \t]<!—[ \t]#(ifndef|ifdef|if)[ \t]+(.?)[ \t](?:—>|!>)(?:[ \t]\n+)?’, end: ‘[ \t]<!(?:—)?[ \t]#endif[ \t](?:—>|!>)(?:[ \t]\n)?’ }, }, js: { if: { start: ‘[ \t](?://|/\)[ \t]#(ifndef|ifdef|if)[ \t]+([^\n])(?:\(?:\|/))?(?:[ \t]\n+)?’, end: ‘[ \t](?://|/\)[ \t]#endif[ \t](?:\(?:\|/))?(?:[ \t]\n)?’ }, } }
将上面代码编译为以下结果<br /><a name="R4joM"></a>### 模板转换uniapp 通过 `mp-vue` 将 `template` 代码解析为 AST 树,如 demo 代码```html<view class="content"><image class="logo" src="/static/logo.png"></image><view><text class="title">{{ title }}</text></view><view>sdfasdfadsf</view></view>

然后使用根据不同平台的环境变量生成不同的编译器,将 AST 树转为对应的小程序类型输出
// uni-template-compiler/lib/index.jsmodule.exports = {compile (source, options = {}) {// ...options.mp.platform = require('./mp')(options.mp.platform)const state = { options };// ...try {res.render = generateScript(traverseScript(ast, state), state)template = generateTemplate(traverseTemplate(ast, state), state)} catch (e) {console.error(e)throw new Error('Compile failed at ' + options.resourcePath.replace(path.extname(options.resourcePath),'.vue'))}// ...}}// uni-template-compiler/lib/mp.jsmodule.exports = function getCompilerOptions (platform) {let id = '@dcloudio/uni-' + platformreturn Object.assign({name: platform},// 基础类型及组件转换baseCompiler,require(id + '/lib/uni.compiler.js'))}// uni-mp-alipay/lib/uni.compiler.jsmodule.exports = {// 事件处理机制,例如将 click 转为小程序 tap 事件getEventType(eventType) {},}
通过 WebpackUniMPPlugin 插件实现生成 WXML 代码
// webpack-uni-mp-loader/lib/plugin/index-new.jsclass WebpackUniMPPlugin {apply (compiler) {// 生成 jsongenerateJson(compilation)// app.js,app.wxssgenerateApp(compilation).forEach(({file,source}) => emitFile(file, source, compilation))// 生成页面 将 div 转为 viewgenerateComponent(compilation, compiler.options.output.jsonpFunction)}}
API 兼容
uniapp 以微信小程序的 API 为标准,将其他将其余平台的 API 通过代理的方式统一代理到 uni 这个全局对象上面,以支付宝小程序为例
// @dcloudio/uni-mp-alipay/dist/index.jsconst protocols = {showToast ({icon = 'success'} = {}) {const args = {title: 'content',icon: 'type',duration: false,image: false,mask: false};if (icon === 'loading') {return {name: 'showLoading',args}}return {name: 'showToast',args}},};// 遍历代理到 uni 对象下Object.keys(my).forEach(name => {if (hasOwn(my, name) || hasOwn(protocols, name)) {// warper 函数做了错误判断及警告uni[name] = promisify(name, wrapper(name, my[name]));}});
自定义组件和静态资源
uniapp 指定工程的目录结构,约束用户通过不同平台的代码或者资源放到对应指定目录下
- 微信小程序原生组件放到
src/wxcomponents目录下 - 不同代码放到
src/platforms/mp-weixin下 - 静态资源放到
src/static/mp-weixin下
通过使用 copy-webpack-plugin 插件将代码复制到 dist
// packages/vue-cli-plugin-uni/lib/copy-webpack-options.jsfunction getCopyWebpackPluginOptions (platformOptions, vueOptions) {const copyOptions = getAssetsCopyOptions(assetsDir).concat(getUniModulesAssetsCopyOptions(assetsDir))global.uniPlugin.copyWebpackOptions.forEach(copyWebpackOptions => {const platformCopyOptions =copyWebpackOptions(platformOptions, vueOptions, copyOptions) || []platformCopyOptions.forEach(copyOption => {if (typeof copyOption === 'string') {copyOption = getAssetsCopyOption(copyOption)}copyOption && copyOptions.push(copyOption)})})// ...return copyOptions}
总结
uniapp 基于 vue-cli 对多端构建的配置进行封装,通过条件编译实现代码分端处理,大大减少了小程序开发成本,代码逻辑结构也值得学习。
