vue-cli4+webpack4+vant搭建前端移动H5通用框架

    技术2022-07-11  95

    vue-cli4+webpack4+vant搭建前端移动H5通用框架(开箱即用)

    简介

    该框架是基于cli4 + webpack4 + vant + less的移动端H5通用框架,可供快速开发。 在此之前需要了解一下一个移动端H5该包含哪些技术点:

    序号要点1cli-42UI框架 vant3rem自适应4请求拦截封装5路由配置6登录校验7环境变量配置8webpack配置(vue.config.js)9弹窗封装(由于不同项目弹框样式的多变性,这里不讲)10性能优化(cnd资源优化、gizp打包优化、可视化、首页骨架屏11html-webpack-plugin使用(埋点封装)12部署

    关于webpack相关的优化方法,可以参考官方文档,这里推荐3篇前端小姐姐的文章(webpack性能优化) 闲话不多说,正片开始。

    1.vue-cli4

    这里不过多解释,贴上(官方文档)

    2.按需引入vant

    vant是有赞的前端团队开源一套产品,分别在业务中台、电商、零售、资产、金融等各个领域都有广泛的使用先例。当然小程序还有套对应的UI框架,为了避免跑题就不解释了。按需引入是我们在使用第三方插件时为了避免全部引入导致打包的提及过大而采用的一种方法, babel-plugin-import 是一款 babel 插件,它会在编译过程中将 import 的写法自动转换为按需引入的方式。这里使用vant官方推荐的引入方式:

    安装插件

    npm i babel-plugin-import -D

    在.babelrc 中添加配置 注意:webpack 1 无需设置 libraryDirectory

    { "plugins": [ ["import", { "libraryName": "vant", "libraryDirectory": "es", "style": true }] ] }

    对于使用 babel7 的用户,可以在 babel.config.js 中配

    module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ], plugins: [ ['import', { libraryName: 'vant', libraryDirectory: 'es', style: true }, 'vant'] ] }

    这里贴上官网的推荐的的引入方法教程(vant快速上手)

    3.rem 适配

    有过移动端开发经历的童鞋肯定理解适配问题,简单来就是让页面在不同大小的设备屏幕上以合适的尺寸展示。我们在收到UI的设计稿时都是px为单位的,这时候将px转化成rem就是rem适配的核心工作啦。 将 html 字跟字体设置为 100px,很多人选择设置为 375px,但是我觉得这样换算出来的 rem 不够精确,而且我们在控制台上调试代码的时候无法很快地口算得出它本来的 px 值。如果设置 1rem=100px,这样我们看到的 0.16rem,0.3rem 就很快得算出原来是 16px,30px 了。

    安装依赖

    npm install px2rem-loader --save-dev

    vue.config.js 进行如下配置

    css: { // css预设器配置项 loaderOptions: { postcss: { plugins: [ require('postcss-px2rem')({ remUnit: 100 }) ] } } },

    在 js中 置 html 跟字体大小

    function initRem() { let cale = window.screen.availWidth > 750 ? 2 : window.screen.availWidth / 375 window.document.documentElement.style.fontSize = `${100 * cale}px` } window.addEventListener('resize', function() { initRem() })

    4.请求拦截封装

    设置请求拦截和响应拦截

    /* 全局默认配置 */ var http = axios.create({ baseURL: baseApi, timeout: 5000 }) /* 请求拦截器 */ http.interceptors.request.use( config => { const guid = generateGuid() const timeStamp = Date.parse(new Date()) / 1000 // 请求头的携带的参数 config.headers = { 'Content-Type': 'application/json;charset=UTF-8', '*****-token': sessionStorage.getItem('token') || '', '*******-call-app-id': globalConfig.apiAppId, '*******-sign': Md5(`call_app_id=${globalConfig.apiAppId}&nonce=${guid}&time_stamp=${timeStamp}&call_app_secret=${globalConfig.apiAppSecret}`) } // 如果每个请求的request Payload都需要携带指定的参数,使用此种方式 config.data = { call_app_id: globalConfig.apiAppId, nonce: guid, time_stamp: timeStamp, ...config.data } return config }, error => { return Promise.reject(error) } ) /* 响应拦截器 */ http.interceptors.response.use( res => { if (res.data.code === 10109) { // token过期 sessionStorage.clear() wxRedirectUrl() } return res.data }, error => { return Promise.reject(error) } )

    封装 get 和 post 请求方法

    function get (url, data, loading) { return new Promise((resolve, reject) => { http.get(url).then( response => { resolve(response.data) }, err => { reject(err) } ) .catch(error => { reject(error) }) }) } function post (url, data, loading) { return new Promise((resolve, reject) => { http.post(url, data, { loading: loading }).then( response => { resolve(response.data) }, err => { reject(err) } ) .catch(error => { reject(error) }) }) } export { http, get, post }

    5.路由配置

    路由的知识点很多,这里不会详细说明,只挑目中用到的地方来介绍,路由的引入方式采用es6提案的import()

    const routes = [ { path: '/', name: 'home', component: () => import('../views/Home'), meta: { title: '小区数字化', keepAlive: false } }, { path: '/about', name: 'About', component: () => import('../views/About'), meta: { title: '其他', keepAlive: false } }, { path: '/login', name: 'Login', component: () => import('../views/Login'), meta: { title: '登录', keepAlive: false } }, { path: '/modify', name: 'ModifyPwd', component: () => import('../views/modify'), meta: { title: '修改密码', keepAlive: false } } ] const router = new VueRouter({ mode: 'history', routes, scrollBehavior: () => ({ y: 0 }) })

    上面的代码相信大家都能看得明白,这里要说一下keepAlive这个属性,它是用来缓存页面增加交互体验和性能的。当一个产品跑过来跟你说,在返回登录页的时候手机号码仍然保留着,不需要用户重新输入,这时候就可以将登录页面的keepAlive来控制页面不需要重新加载。但是,当另一个产品跑过来跟你说首页要埋点,要记录用户的停留时长。毫无疑问你会联想到生命周期函数destroyed,遗憾的是当路由的keepAlive属性true时,destroyed是不执行的,因为路由没有被销毁。所以这个属性切记因地制宜,不要一味地追求用户体验,也不要接到一个需求时只看到表面。当然路由的引入方式还有: 1.Vue异步组件技术:

    { path: '/home', name: 'Home', component: resolve => reqire(['../views/Home.vue'], resolve) }

    2.webpack提供的require.ensure():

    { path: '/home', name: 'Home', component: r => require.ensure([],() => r(require('../views/Home.vue')), 'home') }

    6.登录校验

    这一点有些偏业务了,因为不是每个项目的登录逻辑都类似。但是放这里的原因是想给出一个切实的例子,这样理解起来更容易一些。 我们肯定会经历过这样的场景,就是切换页面是需要判断当前用户是否还权限,或者判断切换页面是是否含有用户信息。如果没有权限或者没有登录信息的情况下我们是要重新授权或者重新登录的。beforeEach钩子函数正好适合这样的场景。

    router.beforeEach((to, from, next) => { const token = sessionStorage.getItem('token') || '' const openId = sessionStorage.getItem('openId') || '' const wxAuthKey1 = getUrlToParam('wxAuthKey1') || sessionStorage.getItem('wxAuthKey1') document.title = to.meta.title if (to.path === '/login') { routerReload(to, next) return } if (!wxAuthKey1 && !openId) { wxRedirectUrl() } else { sessionStorage.setItem('wxAuthKey1', wxAuthKey1) if (!token) { getToken({ key: wxAuthKey1 }).then((res) => { if (res.code === 200) { sessionStorage.setItem('token', res.dataMap.token) sessionStorage.setItem('openId', res.dataMap.openID) routerReload(to, next) } else if (res.code === 700101) { sessionStorage.setItem('openId', res.dataMap.openID) next('/login') } else if (res.code === 700100) { wxRedirectUrl() } else { vm.$toast({ message: res.message }) } }) } else { routerReload(to, next) } } })

    以上代码仅仅使用本人曾经用过的项目,不适用其他项目,不要无脑粘贴复制。 关于验证需要讲两点:

    如果有个需求是当用户过期或者wx的openID没有了(机智的测试会清缓存),需要跳指定跳转到登录页,并且在登录页要to do sth。 首先code为700101是跳转登录页next(’/login’),括号里面是路由的path,接着参数to中去判断是否是登陆页,最后去do sth。这里没有什么重点,需要着重说一下的是下一点。这点有点偏题,但是这个bug很难发现。大家注意到routerReload(to, next),一般next()就行了,那这里为什么我又要去做一层封装呢?原因微信浏览器在ios下的一个bug,下面我给出解释: /** * 解决ios下路由跳转后获取jsTicket失败,刷新页面有恢复的问题 * 这是因为在IOS上,无论路由切换到哪个页面,实际真正有效的的签名URL是【第一次进入应用时的URL】。 * 比如进入应用首页是: https://m.app.com,需要使用JSSDK的页面A是:https://m.app.com/product1/123, * 无论从首页进入到A页面之前,中间跳转过多少次路由,最终签名有效的URL还是首页URL。 * @param {*} to * @param {*} next */ export function routerReload (to, next) { if (isIos() && to.path !== location.pathname) { // 此处不可使用location.replace location.assign(to.fullPath) } else { next() } }

    该问题只有在微信内获取jsTicket才会遇到,如果不需要获取微信jsTicket请绕道。

    7.环境变量配置

    我们开发过程中一般会遇到两个线上环境UAT和PRD,对于那些有测试环境、用户测试环境、准生产环境和生产环境的大厂请容忍我们十八线小厂的穷酸。

    新建.env.*

    .env.development 本地开发环境配置

    NODE_ENV='development' # must start with VUE_APP_ VUE_APP_ENV = 'development'

    env.production 正式环境配置`

    NODE_ENV='production' VUE_APP_ENV = 'production'

    为了在不同环境配置更多的变量,我们在 src 文件下新建一个 config/index

    // 根据环境引入不同配置 process.env.NODE_ENV const config = require('./env.' + process.env.VUE_APP_ENV) module.exports = config

    以上的配置均是为了区别测试环境和生产环境打包而做的区分

    指令动作npm run dev启动本地开发环境npm run build:dev测试环境打包npm run build:prd生产环境打包 "scripts": { "dev": "vue-cli-service serve", "build:dev": "vue-cli-service build --mode development", "build:prd": "vue-cli-service build --mode production", }

    8.webpack配置(vue.config.js)

    尽管cli3以后使用vue.config.js来配置我们项目的东西,但是我还是希望大家有时间去github上看看那些不使用cli3,仍然使用webpack.config.js来配置的项目,因为webpack可是重点中的重点。

    module.exports = { publicPath: './', // 将构建好的文件输出到哪里 outputDir: 'dist', // 放置生成的静态资源(js、css、img、fonts)的目录。 assetsDir: 'static', // 指定生成的 index.html 的输出路径 indexPath: 'index.html', // 是否使用包含运行时编译器的 Vue 构建版本。设置为 true 后你就可以在 Vue 组件中使用 template 选项了,但是这会让你的应用额外增加 10kb 左右。 runtimeCompiler: false, // 默认情况下 babel-loader 会忽略所有 node_modules 中的文件。如果你想要通过 Babel 显式转译一个依赖,可以在这个选项中列出来。 transpileDependencies: [], // 生产环境关闭 source map productionSourceMap: false, // lintOnSave: true, // 配置css css: { // 是否使用css分离插件 ExtractTextPlugin // extract: true, // 不设置 默认生产环境true, 开发环境false sourceMap: true, // css预设器配置项 loaderOptions: { postcss: { plugins: [ require('postcss-px2rem')({ remUnit: 100 }) ] } }, // 启用 CSS modules for all css / pre-processor files. modules: false }, // 是一个函数,会接收一个基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改。 chainWebpack: config => { // 配置别名 config.resolve.alias .set('@', resolve('src')) .set('assets', resolve('src/assets')) .set('components', resolve('src/components')) .set('views', resolve('src/views')) config.optimization.minimizer('terser').tap((args) => { // 去除生产环境console args[0].terserOptions.compress.drop_console = true return args }) // 用于public下index.html的引入外部script脚本配置,引入第三方插件 config.plugin('html').tap((args) => { args[0].aureumaSrc = aureumaConfig.aureumaSrc return args }) const svgRule = config.module.rule('svg') svgRule.uses.clear() svgRule.exclude.add(/node_modules/) svgRule .test(/\.svg$/) .use('svg-sprite-loader') .loader('svg-sprite-loader') .options({ symbolId: 'icon-[name]' }) const imagesRule = config.module.rule('images') imagesRule.exclude.add(resolve('src/components/icon/svg')) config.module.rule('images').test(/\.(png|jpe?g|gif|svg)(\?.*)?$/) }, configureWebpack: (config) => { config.plugins.push(new SkeletonWebpackPlugin({ webpackConfig: { entry: { app: path.join(__dirname, './src/common/entry-skeleton.js') } }, minimize: true, quiet: true, router: { mode: 'history', routes: [ { path: '/', skeletonId: 'skeleton1' }, { path: '/about', skeletonId: 'skeleton2' } ] } })) if (process.env.NODE_ENV === 'production') { config.plugins.push(new BundleAnalyzerPlugin()) config.plugins.push(new CompressionPlugin({ // gzip压缩配置 test: /\.js$|\.html$|\.css/, // 匹配文件名 threshold: 10240, // 对超过10kb的数据进行压缩 deleteOriginalAssets: false // 是否删除原文件 })) } }, // 是否为 Babel 或 TypeScript 使用 thread-loader。该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。 parallel: require('os').cpus().length > 1, // 向 PWA 插件传递选项。 // https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-pwa pwa: {}, devServer: { host: '0.0.0.0', port: 8088, // 端口号 https: false, // https:{type:Boolean} open: true, // 配置自动启动浏览器 open: 'Google Chrome'-默认启动谷歌 hot: true, // 热更新 overlay: false, // 配置多个代理 proxy: { '/api': { target: 'https://www.mock.com', ws: true, // 代理的WebSockets changeOrigin: true, // 允许websockets跨域 pathRewrite: { '^/api': '' } } } } }

    10 性能优化(cnd资源优化、gizp打包优化、首页骨架屏)

    性能优化是非常重要的点,前面介绍了妹子的文章,大家没事可以去消化一下。

    1.CDN资源优化

    cdn对于一个前端来说绝对不是陌生的词汇,但是一点都不熟悉。 CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。 你如果还问我CDN是什么,我会说“我把坚果从一个距离他们很远的筐里盛出来,放在距离他们很近的眼前,让他们不用一次次起身费劲的去抓,而是坐在那儿就能够到"的行为,就是CDN。 如果一个相互后期不停地维护,不停地迭代第三方的包越来遇到,打包后的提及也越来越大,这导致打包后的js体积增大。但是一个公司服务器的带宽是固定的,当公司做广告投放时会冲击服务器的极限,这时候把一些第三方的包改为通过cnd链接获取,也是一种办法。 第一步:

    <body> <div id="app"></div> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> <script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script> <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script> <script src="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js"></script> <script src="https://cdn.bootcss.com/element-ui/2.6.1/index.js"></script> </body>

    第二步:在 vue.config.js 配置 externals 属性

    module.exports = { ··· externals: { 'vue': 'Vue', 'vuex': 'Vuex', 'vue-router': 'VueRouter', 'axios':'axios' } }

    第三步

    npm uninstall vue vue-router vuex axios

    2.gizp打包优化

    所有现代浏览器都支持 gzip 压缩,启用 gzip 压缩可大幅缩减传输资源大小,从而缩短资源下载时间,减少首次白屏时间,提升用户体验。gzip 对基于文本格式文件的压缩效果最好(如:CSS、JavaScript 和 HTML),在压缩较大文件时往往可实现高达 70-90% 的压缩率,对已经压缩过的资源(如:图片)进行 gzip 压缩处理,效果很不好。这里推荐一个图片压缩的网站(TinyPNG)

    const CompressionPlugin = require('compression-webpack-plugin') configureWebpack: (config) => { if (process.env.NODE_ENV === 'production') { config.plugins.push( new CompressionPlugin({ // gzip压缩配置 test: /\.js$|\.html$|\.css/, // 匹配文件名 threshold: 10240, // 对超过10kb的数据进行压缩 deleteOriginalAssets: false, // 是否删除原文件 }) ) } }

    3.首页骨架屏

    这是一个基于 Vue 的 webpack 插件,为单页/多页应用生成骨架屏 skeleton,减少白屏时间,在页面完全渲染之前提升用户感知体验。不知道骨架屏是是什么请点击链接 使用方法: 安装:

    npm install vue-skeleton-webpack-plugin

    新建skeleton.js

    import Vue from 'vue' import Skeleton1 from './Skeleton1' import Skeleton2 from './Skeleton2' export default new Vue({ components: { Skeleton1, Skeleton2 }, template: ` <div> <skeleton1 id="skeleton1" style="display:none"/> <skeleton2 id="skeleton2" style="display:none"/> </div> ` })

    在vue.config.js下配置插件

    const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin') configureWebpack: (config) => { config.plugins.push( new SkeletonWebpackPlugin({ webpackConfig: { entry: { app: path.join(__dirname, './src/common/entry-skeleton.js'), }, }, minimize: true, quiet: true, router: { mode: 'hash', routes: [ { path: '/', skeletonId: 'skeleton1' }, { path: '/about', skeletonId: 'skeleton2' }, ], }, }) ) }

    注意:一定要配置样式分离extract: true

    11.html-webpack-plugin使用(埋点封装)

    我当时做的项目是用公司大数据部门的埋点统计插件(其实就是一个js)来实现的。也就是我们调用这个js暴露出来的方法,分两套环境来实现。

    1.首先就是如何引入这个第三方js

    由于第三方的js没有发布npm仓库上去,所以这里采用传统html引入外部js文件的方式

    <!DOCTYPE html> <html lang="en" style="font-size:100px"> ... <script type="text/javascript" src="<%= htmlWebpackPlugin.options.aureumaSrc %>"></script> </html>
    2.其次如何引入不同环境的js做成配置项

    vue.config.js配置

    const aureumaConfig = require('./src/config/aureumaConfig') module.exports = { chainWebpack: config => { ... config.plugin('html').tap((args) => { args[0].aureumaSrc = aureumaConfig.aureumaSrc return args }) ... }

    aureumaConfig.js

    // 大数据埋点 const prdSrc = 'https://prdSrc.js' const stgSrc = 'https://stgSrc.js' module.exports = { // 埋点地址 aureumaSrc: process.env.NODE_ENV === 'production' ? prdSrc : stgSrc, ... }

    12.部署(IIS)

    部署的的只是很多,路由模式不同部署方式不同,不同的服务器部署方式又不尽相同。这里已history模式在IIS上的部署。 1.首先npm run build:dev 部署测试环境 2.将dist目录下的静态文件放到服务器指定的端口文件夹下(注意publicPath是’/‘还是’./’) 3.hisotry模式会有一个问题,就是刷新出现404.这里贴上解决问题的办法(问题解决)

    参考文章

    淘宝网易移动端px转换rem原理 移动端-VantUI axios封装 vue-cli4链式操作-高级 为vue项目添加骨架屏 搭建移动端H5 vue-cli4 history模式IIS部署 通用框架源码地址

    Processed: 0.009, SQL: 10