首先,小程序原生代码是比较难运行在浏览器端的,想要通过这个思路实现的朋友可以试试wept,但是wept已经不更新了。查阅了一些资料后,最终采用的思路是:小程序使用mpvue开发(一个类vue框架),而mpvue工程转化为vue工程并适配到浏览器。
那么,接下来的问题是:

  1. 如何将mpvue工程打包为vue工程
  2. 打包成vue工程后,需要做哪些适配

如何将mpvue工程打包为vue工程

针对第一个问题,主要思路编写mpvue打包成vue工程的webpack配置即可,期间参考了一下网上的资料总算搞定了。在开始编写配置文件前,首先确认mpvue版本,打开工程根目录package.json,节选如下:

"dependencies": {"axios": "^0.18.0","mpvue": "^1.0.11","mpvue-wxparse": "^0.6.5","vue": "^2.5.16","vue-router": "^3.0.1","vuex": "^3.0.1"},

mpvue新版本的打包配置和旧版有微妙的区别,本文选择的是1.0.11版本。
接下来,大致分为如下几步:

  1. 添加buildH5配置
  2. 添加configH5配置
  3. 代码入口适配
  4. 添加编译命令

添加buildH5配置

标准mpvue工程在根目录下都会有一个build目录。现在为了生成vue工程,需要新建一个buildH5目录。该目录下需要添加如下文件,这些配置几乎和项目没什么关系,不用改动:

build.js

require('./check-versions')()process.env.NODE_ENV = 'production'var ora = require('ora')
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../configH5')
var webpackConfig = require('./webpack.prod.conf')var spinner = ora('building for production...')
spinner.start()rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {if (err) throw errwebpack(webpackConfig, (err, stats) => {spinner.stop()if (err) throw errprocess.stdout.write(stats.toString({colors: true,modules: false,children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.chunks: false,chunkModules: false}) + '\n\n')if (stats.hasErrors()) {console.log(chalk.red('  Build failed with errors.\n'))process.exit(1)}console.log(chalk.cyan('  Build complete.\n'))console.log(chalk.yellow('  Tip: built files are meant to be served over an HTTP server.\n' +'  Opening index.html over file:// won\'t work.\n'))})
})

check-versions.js

'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')function exec (cmd) {return require('child_process').execSync(cmd).toString().trim()
}const versionRequirements = [{name: 'node',currentVersion: semver.clean(process.version),versionRequirement: packageConfig.engines.node}
]if (shell.which('npm')) {versionRequirements.push({name: 'npm',currentVersion: exec('npm --version'),versionRequirement: packageConfig.engines.npm})
}module.exports = function () {const warnings = []for (let i = 0; i < versionRequirements.length; i++) {const mod = versionRequirements[i]if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {warnings.push(mod.name + ': ' +chalk.red(mod.currentVersion) + ' should be ' +chalk.green(mod.versionRequirement))}}if (warnings.length) {console.log('')console.log(chalk.yellow('To use this template, you must update following to modules:'))console.log()for (let i = 0; i < warnings.length; i++) {const warning = warnings[i]console.log('  ' + warning)}console.log()process.exit(1)}
}

dev-client.js

/* eslint-disable */
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')hotClient.subscribe(function (event) {if (event.action === 'reload') {window.location.reload()}
})

utils.js

'use strict'
const path = require('path')
const config = require('../configH5')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')exports.assetsPath = function (_path) {const assetsSubDirectory = process.env.NODE_ENV === 'production'? config.build.assetsSubDirectory: config.dev.assetsSubDirectoryreturn path.posix.join(assetsSubDirectory, _path)
}exports.cssLoaders = function (options) {// console.log(options || {}, options)options = options || {}// console.log('generateLoaders', options.usePostCSS)const cssLoader = {loader: 'css-loader',options: {sourceMap: options.sourceMap}}const postcssLoader = {loader: 'postcss-loader',options: {sourceMap: options.sourceMap}}// generate loader string to be used with extract text pluginfunction generateLoaders (loader, loaderOptions) {const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]// , postcssLoaderif (loader) {loaders.push({loader: loader + '-loader',options: Object.assign({}, loaderOptions, {sourceMap: options.sourceMap})})}// Extract CSS when that option is specified// (which is the case during production build)if (options.extract) {return ExtractTextPlugin.extract({use: loaders,fallback: 'vue-style-loader'})} else {return ['vue-style-loader'].concat(loaders)}}// https://vue-loader.vuejs.org/en/configurations/extract-css.htmlreturn {css: generateLoaders(),postcss: generateLoaders(),less: generateLoaders('less'),sass: generateLoaders('sass', { indentedSyntax: true }),scss: generateLoaders('sass'),stylus: generateLoaders('stylus'),styl: generateLoaders('stylus')}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {const output = []const loaders = exports.cssLoaders(options)for (const extension in loaders) {const loader = loaders[extension]output.push({test: new RegExp('\\.' + extension + '$'),use: loader})}return output
}exports.createNotifierCallback = () => {const notifier = require('node-notifier')return (severity, errors) => {if (severity !== 'error') returnconst error = errors[0]const filename = error.file && error.file.split('!').pop()notifier.notify({title: packageConfig.name,message: severity + ': ' + error.name,subtitle: filename || '',icon: path.join(__dirname, 'logo.png')})}
}

vue-loader.conf.js

'use strict'
const utils = require('./utils')
const config = require('../configH5')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction? config.build.productionSourceMap: config.dev.cssSourceMapmodule.exports = {loaders: utils.cssLoaders({sourceMap: sourceMapEnabled,extract: isProduction}),cssSourceMap: sourceMapEnabled,cacheBusting: config.dev.cacheBusting,transformToRequire: {video: ['src', 'poster'],source: 'src',img: 'src',image: 'xlink:href'}
}

webpack.base.conf.js

'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../configH5')
const vueLoaderConfig = require('./vue-loader.conf')function resolve (dir) {return path.join(__dirname, '..', dir)
}
const createLintingRule = () => ({test: /\.(js|vue)$/,loader: 'eslint-loader',enforce: 'pre',include: [resolve('src'), resolve('test')],options: {formatter: require('eslint-friendly-formatter'),emitWarning: !config.dev.showEslintErrorsInOverlay}
})
module.exports = {context: path.resolve(__dirname, '../'),entry: {app: './src/mainH5.js'// app: './module/' + enterdir + '/src/main.js'},output: {path: config.build.assetsRoot,filename: '[name].js',publicPath: process.env.NODE_ENV === 'production'? '.' + config.build.assetsPublicPath: config.dev.assetsPublicPath},resolve: {extensions: ['.js', '.vue', '.json', '.html'],alias: {'vue$': 'vue/dist/vue.esm.js','@': resolve('src'),'pages': resolve('src/pages'),'packageA': resolve('src/pages/packageA'),'packageB': resolve('src/pages/packageB'),}},module: {rules: [...(config.dev.useEslint ? [createLintingRule()] : []),{test: /\.vue$/,loader: 'vue-loader',options: vueLoaderConfig},{test: /\.js$/,loader: 'babel-loader',include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]},{test: /\.css$/,use: ['vue-style-loader','px2rpx-loader','css-loader'],},{test: /\.less$/,loader: "style-loader!css-loader!less-loader",},{test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,loader: 'url-loader',options: {limit: 1000,name: utils.assetsPath('img/[name].[ext]?v=[hash:7]')}},{test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,loader: 'url-loader',options: {limit: 1000,name: utils.assetsPath('media/[name].[ext]?v=[hash:7]')}},{test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,loader: 'url-loader',options: {limit: 10000,name: utils.assetsPath('fonts/[name].[ext]?v=[hash:7]')}}]},node: {// prevent webpack from injecting useless setImmediate polyfill because Vue// source contains it (although only uses it if it's native).setImmediate: false,// prevent webpack from injecting mocks to Node native modules// that does not make sense for the clientdgram: 'empty',fs: 'empty',net: 'empty',tls: 'empty',child_process: 'empty'}
}

webpack.dev.conf.js

'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../configH5')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)const devWebpackConfig = merge(baseWebpackConfig, {module: {rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: false})},// cheap-module-eval-source-map is faster for developmentdevtool: config.dev.devtool,// these devServer options should be customized in /config/index.jsdevServer: {clientLogLevel: 'warning',historyApiFallback: {rewrites: [{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },],},hot: true,contentBase: false, // since we use CopyWebpackPlugin.compress: true,host: HOST || config.dev.host,port: PORT || config.dev.port,open: config.dev.autoOpenBrowser,overlay: config.dev.errorOverlay? { warnings: false, errors: true }: false,publicPath: config.dev.assetsPublicPath,proxy: config.dev.proxyTable,quiet: true, // necessary for FriendlyErrorsPluginwatchOptions: {poll: config.dev.poll,}},plugins: [new webpack.DefinePlugin({'process.env': require('../configH5/dev.env')}),new webpack.HotModuleReplacementPlugin(),new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.new webpack.NoEmitOnErrorsPlugin(),// https://github.com/ampedandwired/html-webpack-pluginnew HtmlWebpackPlugin({filename: 'index.html',template: 'index.html',inject: true}),// copy custom static assetsnew CopyWebpackPlugin([{from: path.resolve(__dirname, '../static'),to: config.dev.assetsSubDirectory,ignore: ['.*']}]),new CopyWebpackPlugin([{from: path.resolve(__dirname, '../src/app.json'),to: config.dev.assetsSubDirectory + '/../app.json',ignore: ['.*']}]),new CopyWebpackPlugin([{from: path.resolve(__dirname, '../project.config.json'),to: config.dev.assetsSubDirectory + '/../project.config.json',ignore: ['.*']}])]
})module.exports = new Promise((resolve, reject) => {portfinder.basePort = process.env.PORT || config.dev.portportfinder.getPort((err, port) => {if (err) {reject(err)} else {// publish the new Port, necessary for e2e testsprocess.env.PORT = port// add port to devServer configdevWebpackConfig.devServer.port = port// Add FriendlyErrorsPlugindevWebpackConfig.plugins.push(new FriendlyErrorsPlugin({compilationSuccessInfo: {messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],},onErrors: config.dev.notifyOnErrors? utils.createNotifierCallback(): undefined}))resolve(devWebpackConfig)}})
})

webpack.prod.conf.js

'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../configH5/index')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')const env = process.env.NODE_ENV === 'testing'? require('../configH5/test.env'): require('../configH5/prod.env')const webpackConfig = merge(baseWebpackConfig, {module: {rules: utils.styleLoaders({sourceMap: config.build.productionSourceMap,extract: false,usePostCSS: false})},devtool: config.build.productionSourceMap ? config.build.devtool : false,output: {path: config.build.assetsRoot,filename: utils.assetsPath('js/[name].js?v=[chunkhash]'),chunkFilename: utils.assetsPath('js/[id].js?v=[chunkhash]')},plugins: [// http://vuejs.github.io/vue-loader/en/workflow/production.htmlnew webpack.DefinePlugin({'process.env': env}),new UglifyJsPlugin({uglifyOptions: {compress: {warnings: false}},sourceMap: true,parallel: true}),// extract css into its own file//new ExtractTextPlugin({filename: utils.assetsPath('css/[name].css?v=[contenthash]'),// Setting the following option to `false` will not extract CSS from codesplit chunks.// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110allChunks: true,}),// Compress extracted CSS. We are using this plugin so that possible// duplicated CSS from different components can be deduped.new OptimizeCSSPlugin({cssProcessorOptions: config.build.productionSourceMap? { safe: true, map: { inline: false } }: { safe: true }}),// generate dist index.html with correct asset hash for caching.// you can customize output by editing /index.html// see https://github.com/ampedandwired/html-webpack-pluginnew HtmlWebpackPlugin({filename: config.build.index,template: 'index.html',inject: true,minify: false,// minify: {//   removeComments: true,//   collapseWhitespace: true,//   removeAttributeQuotes: true//   // more options://   // https://github.com/kangax/html-minifier#options-quick-reference// },// necessary to consistently work with multiple chunks via CommonsChunkPluginchunksSortMode: 'dependency'}),// keep module.id stable when vendor modules does not changenew webpack.HashedModuleIdsPlugin(),// enable scope hoistingnew webpack.optimize.ModuleConcatenationPlugin(),// split vendor js into its own filenew webpack.optimize.CommonsChunkPlugin({name: 'vendor',minChunks (module) {// any required modules inside node_modules are extracted to vendorreturn (module.resource &&/\.js$/.test(module.resource) &&module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0)}}),// extract webpack runtime and module manifest to its own file in order to// prevent vendor hash from being updated whenever app bundle is updatednew webpack.optimize.CommonsChunkPlugin({name: 'manifest',minChunks: Infinity}),// This instance extracts shared chunks from code splitted chunks and bundles them// in a separate chunk, similar to the vendor chunk// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunknew webpack.optimize.CommonsChunkPlugin({name: 'app',async: 'vendor-async',children: true,minChunks: 3}),// copy custom static assetsnew CopyWebpackPlugin([{from: path.resolve(__dirname, '../static'),to: config.build.assetsSubDirectory,ignore: ['.*']}]),new CopyWebpackPlugin([{from: path.resolve(__dirname, '../src/app.json'),to: config.dev.assetsSubDirectory + '/../app.json',ignore: ['.*']}]),new CopyWebpackPlugin([{from: path.resolve(__dirname, '../project.config.json'),to: config.dev.assetsSubDirectory + '/../project.config.json',ignore: ['.*']}])]
})if (config.build.productionGzip) {const CompressionWebpackPlugin = require('compression-webpack-plugin')webpackConfig.plugins.push(new CompressionWebpackPlugin({asset: '[path].gz[query]',algorithm: 'gzip',test: new RegExp('\\.(' +config.build.productionGzipExtensions.join('|') +')$'),threshold: 10240,minRatio: 0.8}))
}if (config.build.bundleAnalyzerReport) {const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPluginwebpackConfig.plugins.push(new BundleAnalyzerPlugin())
}module.exports = webpackConfig

需要注意的是,webpack.base.conf.js中用了诸如px2rpx-loader、vue-style-loader,如果没有安装会提示报错,照着报错安装下就好了。

添加configH5配置

同样地,在工程根目录下新建configH5目录,目录下需要添加几个文件。

dev.env.js

var merge = require('webpack-merge')
var prodEnv = require('./prod.env')module.exports = merge(prodEnv, {NODE_ENV: '"development"'
})

prod.env.js

module.exports = {NODE_ENV: '"production"'
}

index.js

'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.const path = require('path')module.exports = {dev: {// PathsassetsSubDirectory: 'static',assetsPublicPath: '/',proxyTable: {},// Various Dev Server settingshost: 'localhost', // can be overwritten by process.env.HOSTport: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determinedautoOpenBrowser: false,errorOverlay: true,notifyOnErrors: true,poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-// Use Eslint Loader?// If true, your code will be linted during bundling and// linting errors and warnings will be shown in the console.useEslint: false,// If true, eslint errors and warnings will also be shown in the error overlay// in the browser.showEslintErrorsInOverlay: false,/*** Source Maps*/// https://webpack.js.org/configuration/devtool/#developmentdevtool: 'cheap-module-eval-source-map',// If you have problems debugging vue-files in devtools,// set this to false - it *may* help// https://vue-loader.vuejs.org/en/options.html#cachebustingcacheBusting: true,cssSourceMap: true},build: {// Template for index.htmlindex: path.resolve(__dirname, '../distH5/index.html'),// PathsassetsRoot: path.resolve(__dirname, '../distH5'),assetsSubDirectory: 'static',assetsPublicPath: '/',/*** Source Maps*/productionSourceMap: false,// https://webpack.js.org/configuration/devtool/#productiondevtool: '#source-map',// Gzip off by default as many popular static hosts such as// Surge or Netlify already gzip all static assets for you.// Before setting to `true`, make sure to:// npm install --save-dev compression-webpack-pluginproductionGzip: false,productionGzipExtensions: ['js', 'css'],// Run the build command with an extra argument to// View the bundle analyzer report after build finishes:// `npm run build --report`// Set to `true` or `false` to always turn it on or offbundleAnalyzerReport: process.env.npm_config_report}
}

代码入口适配

细心看过上面那些配置文件的人应该已经发现,入口是这样配置的:

entry: {app: './src/mainH5.js'// app: './module/' + enterdir + '/src/main.js'},

下面实现mainH5.js

import Vue from 'vue'
import App from './AppH5'
import router from './router'
import global from './utils/global.js'
import store from './store/index.js'
import {loginUtils} from './utils/LoginUtils'
// import '../static/fonts/icomoon.css'// 试工程实际情况,这里可能有项目独特的配置,总之把main.js复制过来就好了
Vue.use(global);
Vue.prototype.$store = store;
Vue.config.productionTip = false;
Vue.prototype.$customEvent = new Vue(App);
App.mpType = 'app';
Vue.mixin(loginUtils)
Vue.prototype.$bus = new Vue()
Vue.config.productionTip = false/* eslint-disable no-new */
let vue = new Vue({el: '#app',router,components: { App },template: '<div><App/></div>',store,
})window.router = router

似乎和main.js没有太大区别?没有关系,稍后就有新建一个入口文件的必要了。这里的一个区别是:

import App from './AppH5'

引入的不是默认的App.vue呢。那么接下来新建AppH5.vue,放在mainH5.js同级目录下:

<template><div id="app"><router-view></router-view></div>
</template><script>export default {name: 'App'}
</script>
<style>
</style>

可以看到这里用了router。mpvue开发小程序大概率不会用router,因为有wx.navigateTo这种原生api,但是浏览器可不会识别wx api,因此路由需要由router实现。
在src下新建一个router目录,目录下新建一个index.js,在这个文件中,把所有页面注册:

import Vue from 'vue'
import Router from 'vue-router'
import main from '../pages/main/index'
import my from '../pages/my/index'Vue.use(Router)export default new Router({routes: [{path: '/',name: 'main',component: main,alias: '/pages/main/main'},{path: '/pages/my/main',name: 'my',component: my}]
})

这里只是一个示例,实际项目要把所有页面写到这个配置中。

添加编译命令

就像npm run dev、npm run build一样,我们需要一个相似的命令生成vue工程。修改package.json,就像这样:

"scripts": {"dev": "node build/dev-server.js","start": "node build/dev-server.js","build": "node build/build.js","devH5": "webpack-dev-server --iframe --progress --config buildH5/webpack.dev.conf.js","all": "npm run dev && npm run devH5","buildH5": "node buildH5/build.js"},

现在可以用npm run devH5生成项目在webpack-dev-server上的预览,或用npm run buildH5生成vue工程了,只不过生成出来的东西暂时没法跑,应该全是报错吧(笑),接下来需要做一些适配工作。

小程序工程适配为vue工程

之前提到过,wx api在浏览器端不识别,这个显然需要适配。然而除了这个,小程序的生命周期、组件也是问题。

适配wx api

首先需要新建utils/wx.js,将所有用到的wx api封装起来:

  getParams: function (target) {return target.$route.query},redirectTo: function (url, target) {let route = target.$router.matcher.match(url.url)if (route) {window.parent.onRedirectTo(route)}target.$router.replace(url.url)},switchTab: function (url, target) {let route = target.$router.matcher.match(url.url)if (route) {window.parent.onSwitchTab(route)}target.$router.replace(url.url)},navigateBack: function (delta, target) {let r = window.parent.onNavigateBack(delta)target.$router.replace(r.path)},reLaunch: function (url, target) {let route = target.$router.matcher.match(url.url)if (route) {window.parent.onRelaunch(route)}target.$router.replace(url.url)},getSystemInfoSync() {return {screenWidth: 750,screenHeight: 1334,windowWidth: 750,windowHeight: 1334}},

这里只展示了部分wx api在浏览器端的实现,有需要的自行实现或适配。
注意getParams,在mpvue中,接收上一个页面传的query参数可以这么写:

mounted () {let id = this.$root.$mp.query.id
}

但这种方式并不适用于vue,所以改为用router的方式获取参数,而页面组件中,调用封装的getParams方法即可。

适配生命周期

小程序中onLoad、onShow、onUnLoad等生命周期在vue里可是行不通的。当然,新项目可以通过开发上的约定,例如必须使用vue生命周期、不使用小程序生命周期来适配,而老项目一个个改就显得麻烦了。因此,这里的思路是在vue的runtime里加入小程序的生命周期,即定制。
这里需要修改node_modules/vue/dist/vue.esm.js:

var LIFECYCLE_HOOKS = ['beforeCreate','created','onLoad', // 加入生命周期'onShow', // 加入生命周期'beforeMount','mounted','beforeUpdate','updated','beforeDestroy','onUnload', // 加入生命周期'destroyed','activated','deactivated','errorCaptured'
];

还是这个文件,修改initMixin方法:

vm._self = vm;initLifecycle(vm);initEvents(vm);initRender(vm);callHook(vm, 'beforeCreate');initInjections(vm); // resolve injections before data/propsinitState(vm);initProvide(vm); // resolve provide after data/propscallHook(vm, 'created');callHook(vm, 'onLoad'); // 调起生命周期callHook(vm, 'onShow'); // 调起生命周期/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {vm._name = formatComponentName(vm, false);mark(endTag);measure(("vue " + (vm._name) + " init"), startTag, endTag);}if (vm.$options.el) {vm.$mount(vm.$options.el);}// 下略

找到Vue.prototype.$destroy,在代码里加入一行onUnload的调起:

Vue.prototype.$destroy = function () {var vm = this;if (vm._isBeingDestroyed) {return}callHook(vm, 'beforeDestroy');callHook(vm, 'onUnload'); // 调起生命周期vm._isBeingDestroyed = true;// remove self from parentvar parent = vm.$parent;if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {remove(parent.$children, vm);}// 下略
}

现在vue可以调起小程序的生命周期了。

适配组件

小程序原生开发了许多组件,这些组件作为html标签当然无法被浏览器识别,有些甚至和html冲突(例如小程序的input,在html里也有个同名的)。这里适配有相当的工作量,因为组件之多、组件的属性、方法之多是需要一个团队来完成的。然而,要达到预览效果的话,仅需要适配常用组件已经常用的属性就行了。

以小程序image标签为例。首先,在工程src下新建componentsH5目录,目录下新建wx-image.vue:

<template><div ref="img_loader" v-if="mode !== 'aspectFix'" style="display: flex; flex-direction: row; justify-content: center; align-items: center; overflow: hidden;"><img :src="resolvedSrc" :calprop="resolveSize" :style="rStyle" /></div><div ref="img_loader" v-else style="display: flex; flex-direction: column; justify-content: center; align-items: center; overflow: hidden;"><img :src="resolvedSrc" :calprop="resolveSize" :style="rStyle" /></div>
</template><script>export default {name: "wx-image",props: {src: {type: String},mode: {type: String}},data () {return {rStyle: {}}},computed: {resolvedSrc () {if (!this.src) {return this.src}if (this.src.startsWith(('/static'))) {return '.' + this.src} else {return this.src}},resolveSize () {if (!this.src) {this.rStyle = {width: '100%', height: '100%'}return}let self = thislet img = new Image()img.src = self.srclet mode = self.modelet imgWidth = img.widthlet imgHeight = img.heightlet resolvedWidth = imgWidthlet resolvedHeight = imgHeightsetTimeout(function () {let border = self.$refs.img_loader.getBoundingClientRect()let borderWidth = border.widthlet borderHeight = border.height || imgHeightif (borderHeight === 0 || imgHeight === 0) {self.rStyle =  {width: '100%', height: '100%'}return}if (!mode) {self.rStyle =  {width: borderWidth + 'px', height: borderHeight + 'px'}return} else if (mode === 'scaleToFill') {self.rStyle =  {width: '100%', height: '100%'}return} else if (mode === 'aspectFit') {console.log('aspectFit');// 保持横纵比缩放,长边显示if (borderWidth / borderHeight > imgWidth / imgHeight) {resolvedWidth = imgWidth / imgHeight * borderHeightresolvedHeight = borderHeightconsole.log('aspectFit1', borderWidth, borderHeight, resolvedWidth, resolvedHeight);} else {resolvedWidth = borderWidthresolvedHeight = borderWidth * imgHeight / imgWidthconsole.log('aspectFit2', borderWidth, borderHeight, resolvedWidth, resolvedHeight);}} else if (mode === 'aspectFill') {// 保持横纵比缩放,长边裁剪if (borderWidth / borderHeight > imgWidth / imgHeight) {resolvedWidth = borderWidthresolvedHeight = borderWidth * imgHeight / imgWidth} else {resolvedWidth = imgWidth / imgHeight * borderHeightresolvedHeight = borderHeight}} else if (mode === 'aspectFix') {// 宽度不变,高度自适应resolvedWidth = borderWidthresolvedHeight = borderWidth * imgHeight / imgWidth}self.rStyle =  {width: resolvedWidth + 'px', height: resolvedHeight + 'px'}})return {}}}}
</script><style scoped></style>

在这里,适配了image两个最重要的属性:src和mode。注意,计算属性resolvedSrc将"/static"开头的传入值自动转化成了"./static",这是因为生成的vue工程放在服务端并作为静态资源暴露的时候,写作"/static/*"格式的本地图片无法显示,所以适配成更兼容的格式。另一方面,mode对小程序中4大缩放模式做了简单的适配,保证显示效果。

那么,我们只要全局注册一个名为的image组件,其实现是wx-image.vue就好了!修改mainH5.js:

// 引入组件库
import wxImage from './componentsH5/wx-image'// 全局mixin
Vue.mixin({components: {'image': wxImage}
})

之前新建mainH5.js而不是直接用main.js的理由在此,我们需要在这里一一注册我们的适配组件。
但是很遗憾,这样无法通过vue的编译,查看vue源码发现,vue禁止image作为组件名:

export function validateComponentName (name: string) {if (!/^[a-zA-Z][\w-]*$/.test(name)) {warn('Invalid component name: "' + name + '". Component names ' +'can only contain alphanumeric characters and the hyphen, ' +'and must start with a letter.')}if (isBuiltInTag(name) || config.isReservedTag(name)) {warn('Do not use built-in or reserved HTML elements as component ' +'id: ' + name)}
}

这里方法1是修改vue源码,去掉这个限制,思路2是修改打包,将image标签自动转化为wx-image标签。这里选择的是方法2,找到node_modules/vue-template-compiler/build.js,搜索2403行左右的parseHTML,如下修改:

parseHTML(template, {warn: warn$1,expectHTML: options.expectHTML,isUnaryTag: options.isUnaryTag,canBeLeftOpenTag: options.canBeLeftOpenTag,shouldDecodeNewlines: options.shouldDecodeNewlines,shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,shouldKeepComment: options.comments,start: function start (tag, attrs, unary) {// check namespace.// inherit parent ns if there is onevar ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);// 将微信小程序组件简单适配到h5// 添加的代码if (tag === 'view') {tag = 'div'}if (tag === 'image') {tag = 'wx-image'}// 添加的代码结束// handle IE svg bug/* istanbul ignore if */if (isIE && ns === 'svg') {attrs = guardIESVGBug(attrs);}// 下略

修改后,image标签就会被自动转化了。最后别忘了修改mainH5.js:

Vue.mixin({components: {wxImage // 修改这里}
})

这样就完成了image标签的基本适配,其他组件用相似的思路就行了。
这里再举例一个scroll-view的简单适配:

<template><div :class="containerStyle"><slot></slot></div>
</template><script>export default {name: "scroll-view",props: {scrollX: {type: Boolean,default: false},scrollY: {type: Boolean,default: false}},computed: {containerStyle () {if (this.scrollX) {return 'sv_x'}if (this.scrollY) {return 'sv_y'}return ''}}}
</script><style scoped>.sv_x {overflow-x: auto;overflow-y: hidden;display: flex;flex-direction: row;}.sv_y {overflow-x: hidden;overflow-y: auto;display: flex;flex-direction: column;flex-wrap: nowrap;}
</style>

常见的组件如view、image、scroll-view、text、swiper等适配后,至少大部分布局显示是不成问题的。

适配单位

小程序一般使用rpx作为单位开发,而rpx在浏览器上无法识别。如何在尽可能不修改项目代码的同时做到适配呢?
一般来说,项目代码中用到rpx的有这么几种情况:
1 style内联

<divstyle="width: 750rpx; height: 50rpx;"></div>

2 style内联(动态)

<div :style="{width: divWidth + diffWidth + 'rpx'}"></div>

3 style标签中

<style>.container {width: 750rpx;
}</style>

4 代码中

let divStyle = {width: divWidth + diffWidth + 'rpx'}

首先,对于2、4情况,想要自动实现只能通过编写脚本进行文本替换,然而实际情况复杂,所以写脚本是吃力不讨好的,实际开发中尽量避免,如果用到这种动态情况,用vw这种兼容更强的单位就好。对于1、3情况,是有办法解决的。
修改node_modules/lib/loader.js:

processCss(content, map, {mode: moduleMode ? "local" : "global",from: loaderUtils.getRemainingRequest(this).split("!").pop(),to: loaderUtils.getCurrentRequest(this).split("!").pop(),query: query,resolve: resolve,minimize: this.minimize,loaderContext: this,sourceMap: sourceMap}, function(err, result) {if(err) return callback(err);var cssAsString = JSON.stringify(result.source);// 这里添加一行,替换rpx为pxcssAsString = cssAsString.replace(/rpx/g, 'px')// for importing CSSvar importUrlPrefix = getImportPrefix(this, query);// 下略

这样style标签中的rpx被自动转化了。我们只需要将预览窗口的宽度定为750px,即可达到适配目的。预览窗口本身可以scale缩放,来适应浏览器的窗口大小。

另外,对于情况1,我们可以写一个类似px2rpx-loader的插件来实现转换,但是更简单粗暴的方法是直接修改node_modules/px2rpx/bin/px2rpx.js,将其实现从"px->rpx"改为"rpx->px":

#!/usr/bin/env nodevar program = require('commander');
var pkg = require('../package.json');
var Px2rpx = require('../index');
var chalk = require('chalk');
var path = require('path');
var fs = require('fs-extra');// string to variables of proper type(thanks to zepto)
function deserializeValue(value) {var num;try {return value ?value == "true" || value == true ||(value == "false" || value == false ? false :value == "null" ? null :!/^0/.test(value) && !isNaN(num = Number(value)) ? num :/^[\[\{]/.test(value) ? JSON.parse(value) :value): value;} catch (e) {return value;}
}function saveFile(filePath, content) {fs.createFileSync(filePath);fs.writeFileSync(filePath, content, {encoding: 'utf8'});console.log(chalk.green.bold('[Success]: ') + filePath);
}program.version(pkg.version).option('-u, --rpxUnit [value]', 'set `rpx` unit value (default: 75)', 75).option('-x, --threeVersion [value]', 'whether to generate @1x, @2x and @3x version stylesheet (default: false)', false).option('-r, --rpxVersion [value]', 'whether to generate rpx version stylesheet (default: true)', true).option('-b, --baseDpr [value]', 'set base device pixel ratio (default: 2)', 2).option('-p, --rpxPrecision [value]', 'set rpx value precision (default: 6)', 6).option('-o, --output [path]', 'the output file dirname').parse(process.argv);if (!program.args.length) {console.log(chalk.yellow.bold('[Info]: ') + 'No files to process!');return false;
}var config = {rpxUnit: deserializeValue(program.rpxUnit),threeVersion: deserializeValue(program.threeVersion),rpxVersion: deserializeValue(program.rpxVersion),baseDpr: deserializeValue(program.baseDpr),rpxPrecision: deserializeValue(program.rpxPrecision)
};var px2rpxIns = new Px2rpx(config);program.args.forEach(function (filePath) {if (path.extname(filePath) !== '.css') {return;}var cssText = fs.readFileSync(filePath, {encoding: 'utf8'});var outputPath = program.output || path.dirname(filePath);var fileName = path.basename(filePath);// generate @1x, @2x and @3x version stylesheetif (config.threeVersion) {for (var dpr = 1; dpr <= 3; dpr++) {var newCssText = px2rpxIns.generateThree(cssText, dpr);var newFileName = fileName.replace(/(.debug)?.css/, dpr + 'x.debug.css');var newFilepath = path.join(outputPath, newFileName);saveFile(newFilepath, newCssText);}}// generate rpx version stylesheetif (config.rpxVersion) {var newCssText = px2rpxIns.generaterpx(cssText);var newFileName = fileName.replace(/(.debug)?.css/, '.debug.css');var newFilepath = path.join(outputPath, newFileName);saveFile(newFilepath, newCssText);}
});

结束语

本文提供了一个小程序在浏览器端运行的思路,其中适配组件和api是工作量大的地方,但由于一些原因,我不会在Github放demo,想要真正商业级实现这个需求就看自身了。

微信小程序如何在浏览器运行相关推荐

  1. 手把手教你迁移微信小程序到 QQ 浏览器!

    继微信.QQ 之后,QQ 浏览器上也可以使用小程序了. 12 月 5 日,QQ浏览器小程序宣布,实现与微信小程序打通.QQ 浏览器 Android 版现已上线小程序,在搜索的场景下,小程序嵌入 QQ ...

  2. 微信小程序(safair浏览器)flex布局中的坑

    今天在用微信小程序做flex布局的时候遇到了一些问题. 布局简单来说是这样的,最外层是一个flex布局属性为flex-direction:column的元素.里面有未设置height,并且flex-g ...

  3. 微信小程序学习—配置HBuilder运行微信小程

    第一次接触微信小程序开发.在网上找了个项目想要运行,遇到了一些问题,记录一下运行的问题,以及解决方式,供大家参考. 遇到的报错 于是上网搜索结果 第一步 在微信小程序中打开 设置-安全设置 打开服务端 ...

  4. 用git拉取微信小程序项目到本地运行(简单实用)

    一.找到git项目的页面,点击复制链接 二.在本地磁盘找一个文件夹存放你项目的路径,右键选择Git Bash 然后会弹出git命令窗口,输入 git clone 右键选择Paste粘贴你复制的链接 一 ...

  5. 【微信小程序】根据当前运行环境调用不同的接口地址的一些方法

    问题描述 在项目的不同阶段,需要调用不同环境的接口,然后小程序目前并未提供这个很重要的功能. 解决方法 目前没有找到非常满意的方法,提供两个妥协方案 1.不同环境配置不同的域名,通过全局变量控制,发布 ...

  6. 基于安卓的掌上校园系统|食堂缴费图书馆预约【可微信小程序与android studio运行】

    <[含文档+PPT+源码等]精品基于Uniapp+SSM实现的安卓的掌上校园系统[包运行成功]>该项目含有源码.文档.PPT.配套开发软件.软件安装教程.项目发布教程等 软件开发环境及开发 ...

  7. 基于android的农产品销售App平台【可微信小程序与android studio运行】

    <[含文档+PPT+源码等]精品基于Uniapp+SSM实现的移动端农副产品销售平台实现的App[包运行成功]>该项目含有源码.文档.PPT.配套开发软件.软件安装教程.项目发布教程等 软 ...

  8. 基于android在线点单系统APP餐饮餐厅订餐点餐【可微信小程序与android studio运行】

    <[含文档+PPT+源码等]精品基于Uniapp+SSM实现的android在线点单系统APP[包运行成功]>该项目含有源码.文档.PPT.配套开发软件.软件安装教程.项目发布教程等 软件 ...

  9. 基于android的美食餐厅订餐点餐APP丨【可微信小程序与android studio运行】

    <[含文档+PPT+源码等]精品基于Uniapp实现的美食APP[包运行成功]>该项目含有源码.文档.PPT.配套开发软件.软件安装教程.项目发布教程等 软件开发环境及开发工具: 开发语言 ...

最新文章

  1. PHP 端口号 是否 被占用 以及 解决方法
  2. Linux漏洞建议工具Linux Exploit Suggester
  3. Openstack组件部署 — Networking service_Compute Node
  4. 人脸变形算法——MLS
  5. 经典面试题(15):以下代码将输出的结果是什么?
  6. JVM专题之类加载机制
  7. 洛谷 1115——最大子段和(线性数据结构)
  8. 序列化之XML序列化技术
  9. Chrome浏览器添加fehelper插件
  10. 使用kali破解win7密码
  11. 5个步骤实现流程管理
  12. scsi接口服务器硬盘转速,服务器硬盘接口SCSI结构、特点详解
  13. 【PYTHON,WORD】3.调整Word文档样式
  14. Java实现office转PDF文件支持全部转换及Excel转换乱码和格式错乱解决
  15. 实体完整性检查和违约处(B+树索引介绍)
  16. 戴尔服务器的安装维护和调试,服务器的安装与维护技巧——数据湾
  17. PERL XS tutorial
  18. abp+dapper+mysql_ABP架构学习系列四:集成Dapper
  19. Qt 制作安装程序(使用 binarycreator.exe)
  20. 安卓入门,简单画图板的实现

热门文章

  1. 《C#设计模式》--03.代理设计模式(结构型设计模式)
  2. 使用Excel自动生成sql语句
  3. 如何成为抖音带货达人?抖音带货6点必备技巧
  4. HTML5 Video(视频),HTML 音频(Audio)
  5. 计算机用户文件夹迁移,Win7系统通过注册表将用户目录转移到D盘或其他盘的方法...
  6. shell脚本编程之条件语句【二】(跟着小张一起走)
  7. 如何通过设计验证让SoC芯片流片成功
  8. 关键词搜索获取商品数据方法
  9. 微信公众测试号php配置失败,微信测试号的接口配置信息--配置失败
  10. 重启服务器后磁盘显示空余变大,(已解决)开机后发现服务中Superfetch服务项会导致磁盘利用率在85%以上,重启后依旧...