【vue多页面】h5活动页我是这样整合的__Vue.js
发布于 4 年前 作者 banyungong 2065 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

学习总结篇,以能否造轮子衡量学习效果。最近一直在做活动页面(+++)o(╥﹏╥)o

[Vue多页面项目h5]活动页模板传送门github

背景

大多数情况下,我们使用 webpack来打包单页应用程序,这个时候只需要配置一个入口,一个模板文件,但也不尽是如此,有时候也会碰到多页面的项目,比如一些生命周期较短的活动页:

活动页需求

移动端h5经常遇到的情况就是,活动多,迭代快。但单页面的情况显然会不适用,主要体现在以下几个方面:

  1. 重复建立项目,手动😭添加,效率低。
--project1
---node_modules
---src
----style
----common
----components
----views
---package.json

--project2
---node_modules
---src
----style
----common
----components
----views
  1. 我司的活动页h5有大量的共性:

    • 页面自适应
    • 大量基础components(如:loading,dialog,btn,海报制作等)
    • 大部分主题一致
  2. 新活动页上线部署需要重复配置jenkins,效率低

基于以上的现状,此时多页面的优势就显现出来了,下面谈谈,经过多页面整合后vue-multi-page的效果

vue-multi-page使用说明

活动页h5多页面配置,并结合jenkinsnginx实现,每次增加一个页面,只需要在jenkins的动态参数中增加一个参数

项目结构:

1. 在pages下建立自己的页面

--pages
---demo
----index.html
----app.vue
----main.js

2. 项目启动和打包

npm install
npm run serve --page=pages/demo
npm run build --page=pages/demo
npm run lint --page=pages/demo

开发访问

npm run serve --page=pages/demo
http://localhost:8888/demo.html#/home

3. nginx配合

nginx只需要配置个location的根目录即可。

root指向打包后的dist即可

	location / {
    	root xxx/dist;
        index index.html;
    }

这样,新增活动页,只需要在pages下新增目录即可,就可以直接访问了:

`http://www.bai.cn/page1/#/home`

`http://www.bai..cn/page2/#/home`

如果持续集成使用的是jenkins,则还可以借助jenkinsChoice Parameter

  1. Choice Parameter增加选项
page1

page2

  1. 构建的时候直接选择相应页面即可。

3. 访问地址

http://www.bai.cn/page1/#/home

http://www.bai.cn/page2/#/home

4. 内置环境变量

标识 描述
DEV 开发环境
TEST 测试环境
PRE 预发布环境
PROD 生产环境

5. vue-cli参考链接

See Configuration Reference.

vue-multi-page配置说明

1. entry入口配置

vue-clipage属性让我们能定义多个入口文件,借助这个属性,实现活动页多页面的整合。

每个页面都是 src/目录下的一个文件夹,这个文件夹中有两个子目录,分别存放这个页面的模板 html,样式文件 css,还有一个入口文件 index.js

既然有规则,那么肯定是可以进行程序编码的,如果按照这种规则,每个页面都是 ./src下的一个目录,目录名即为页面名,并且这个目录中的结构也都是相同的,那么可以通过一个通用方法来获取所有的页面名称(例如 bargain、demo),这个通用方法的一个示例如下:

const glob = require('glob')
const path = require('path')

/**
 * @param {*String} filterPath 
 * @param {*String} filterStr 
 */
function getEntry (filterPath, filterStr) {
  let globPath = filterPath
  let files = glob.sync(globPath)
  let dirname, entries = {}
  for (let i = 0; i < files.length; i++) {
    dirname = path.dirname(files[i])
    if (dirname.includes(filterStr)) {
      entries['index'] = {
        entry: dirname + '/main.js',
        template: dirname + '/index.html'
      }
      break
    }
  }
  console.log('getEntry:', entries)
  return entries
}

// 输入
getEntry('src/pages/**/*.html', getNPMParams().page)

// 输出
{
  index: {
    entry: 'src/pages/demo/main.js',
    template: 'src/pages/demo/index.html'
  }
}

借助glob这个库,遍历 .src/目录下,具有这种规律src/pages/**/*.html的子目录,通过正则匹配出这个子目录的名称。

获取到了所有的页面名称,下面就好办了。

接着,我们要打包某个pages/demo是通过配置npm命令参数实现:

npm run serve --page=pages/demo

npm run build --page=pages/demo

npm run lint --page=pages/demo

这样,只要通过process.argv获取到了npm命令行的参数,就可以知道,当前需要运行或者构建的是哪个页面了。

具体获取方法如下:

function getNPMParams() {
  let argv
  try {
    argv = JSON.parse(process.env.npm_config_argv).original
  } catch (ex) {
    argv = process.argv
  }
  console.log('argv----', argv)
  const params = {}
  argv &&
  argv.forEach(item => {
    const arr = item.split(/=/gi)
    if (item.slice(0, 2) === '--' && arr.length === 2) {
      params[arr[0].slice(2)] = arr[1]
    }
  })
  if (params && params.page) {
    if (!fs.existsSync(path.resolve(__dirname, '../src/', params.page))) {
      console.log(`${params.page}不存在,请检查pages下是否有该目录`)
      process.exit()
    }
  } else {
    console.log('输入格式请参考:npm run serve --page=pages/xxxx')
    process.exit()
  }
  return params
}

vue.config.js 具体配置

const { getEntry, getNPMParams } = require('./webpack/utils')
const entry = getEntry('src/pages/**/*.html', getNPMParams().page)
const IS_PRODUCTION = process.env.ENV === 'prod'
const port = 8888
const pageName = getNPMParams().page.split('/')[1]

module.exports = {
  publicPath: './',
  lintOnSave: !IS_PRODUCTION,
  // 根据入口构建
  pages: entry,
  // 自定义输出
  outputDir: 'dist/' + pageName,
  devServer: {
    port: port,
    disableHostCheck: true
    // compress: true // GZIP
  }
}

2. 环境配置

其实vue-cli已经提供了模式和环境的配置

特地地讲,是因为,vue-cli提供的模式和环境的配置,是根据不同环境分布在不同的env文件中,这样不方便比对和查阅。

所以,这个整合项目是采用cross-envDefinePlugin,通过注入环境变量的方式进行环境配置。完成后只会有一个env.js的配置:

const localConfig = {
  DEV: {
    ENV: 'dev',
    BASE_API: '',
  },
  TEST: {
    ENV: 'test',
    BASE_API: '',
  },
  PRE: {
    ENV: 'pre',
    BASE_API: '',
  },
  PROD: {
    ENV: 'prod',
    BASE_API: '',
  }
}

module.exports = (conf => {
  const systemEnvs = ["DEV", "PROD", "TEST", "PRE"]

  systemEnvs.forEach(env => {
    conf[env] = Object.assign(
      {
        BASE_API: "/",
        PROCESS_ENV: env.toLocaleLowerCase(),
        NODE_ENV: "production"
      },
      conf[env] || {}
    )
  })
  return conf
})(localConfig)

接着,在vue.config.jschainWebpack中注入环境变量

const path = require('path')

const { getEntry, getNPMParams } = require('./webpack/utils')
const entry = getEntry('src/pages/**/*.html', getNPMParams().page)
const IS_PRODUCTION = process.env.ENV === 'prod'
const ENV_CONFIG = require('./config/env')
const port = 8888
const pageName = getNPMParams().page.split('/')[1]

module.exports = {
  publicPath: './',
  lintOnSave: !IS_PRODUCTION,
  // 根据入口构建
  pages: entry,
  // 自定义输出
  outputDir: 'dist/' + pageName,
  devServer: {
    port: port,
  },
  chainWebpack: config => {
    // 注入环境变量
    config.plugin('define').tap(args => {
      args[0]['process.env'] = JSON.stringify(ENV_CONFIG[(process.env.PROCESS_ENV).toLocaleUpperCase()])
      return args
    })
  }
 }

3. flexible配置

自适应的方案采用的是flexible方案,配置如下:

module.exports = {
  publicPath: './',
  lintOnSave: !IS_PRODUCTION,
  // 根据入口构建
  pages: entry,
  // 自定义输出
  outputDir: 'dist/' + pageName,
  devServer: {
    port: port,
    disableHostCheck: true
    // compress: true // GZIP
  },
  css: {
    loaderOptions: {
      css: {},
      postcss: {
        plugins: [
          require('postcss-pxtorem')({
            rootValue: 75, // 换算的基数(设计图750的根字体为75)
            selectorBlackList: ['.van'], // 要忽略的选择器并保留为px。
            // 要忽略的选择器并保留为px。 selectorBlackList: ['.van'], // 要忽略的选择器并保留为px。
            propList: ['*'], // 可以从px更改为rem的属性。
            minPixelValue: 2 // 设置要替换的最小像素值。
          })
        ]
      }
    }
  }
}

4. 其它优化配置

  1. 抛弃 DLL,选择hard-source-webpack-plugin

vue-clicreate-react-app中可以知道并没有实用dll,是因为:Webpack 4 的打包性能足够好的,dll继续维护的必要了。

更好的代替者DLL,选择hard-source-webpack-plugin

  chainWebpack: config => {
  // 启用缓存
  config.plugin('hardSource')
    .use(new HardSourceWebpackPlugin())
 }
  1. gzip配置,terser清除控制台
const path = require('path')
const CompressionWebpackPlugin = require('compression-webpack-plugin')

const { getEntry, getNPMParams } = require('./webpack/utils')
const entry = getEntry('src/pages/**/*.html', getNPMParams().page)
const IS_PRODUCTION = process.env.ENV === 'prod'
const ENV_CONFIG = require('./config/env')
const port = 8888
const pageName = getNPMParams().page.split('/')[1]

module.exports = {
  publicPath: './',
  lintOnSave: !IS_PRODUCTION,
  // 根据入口构建
  pages: entry,
  // 自定义输出
  outputDir: 'dist/' + pageName,
  devServer: {
    port: port,
    disableHostCheck: true
    // compress: true // GZIP
  },
  chainWebpack: config => {
    if (IS_PRODUCTION) {
      // 开启gzip,需要配置nginx
      config.plugin('compressionPlugin')
        .use(new CompressionWebpackPlugin({
          asset: '[path].gz[query]',
          algorithm: 'gzip',
          test: /\.(js|css|html|svg)$/,
          threshold: 10240, // 大于10K才压缩gzip
          minRatio: 0.8 // 压缩比例(minRatio = Compressed Size / Original Size)
        }))
      // 清除生产环境清除控制台输出
      config.optimization.minimizer('terser').tap((args) => {
        args[0].terserOptions.compress.drop_console = true
        return args
      })
    }
  },
  // 生产环境关闭sourceMap
  // productionSourceMap: !IS_PRODUCTION,
}

项目工具类

  1. 各端判断工具browser.js。主要通过navigator.userAgent
/* 判断浏览器类型 */
export const browser = {
  versions: (function() {
    var u = navigator.userAgent
    return {
      trident: u.indexOf('Trident') > -1, // IE内核
      presto: u.indexOf('Presto') > -1, // opera内核
      webKit: u.indexOf('AppleWebKit') > -1, // 苹果、谷歌内核
      gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') === -1, // 火狐内核
      mobile: !!u.match(/AppleWebKit.*Mobile.*/), // 是否为移动终端
      ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), // ios终端
      android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1, // android终端
      iPhone: u.indexOf('iPhone') > -1, // 是否为iPhone或者QQHD浏览器
      iPad: u.indexOf('iPad') > -1 || u.indexOf('Macintosh') > -1, // 是否iPad
      webApp: u.indexOf('Safari') === -1, // 是否web应该程序,没有头部与底部
      weixin: u.indexOf('MicroMessenger') > -1, // 是否微信 (2015-01-22新增)
      qq: u.indexOf(' QQ') > -1 // 是否QQ
    }
  }()),
  language: (navigator.browserLanguage || navigator.language).toLowerCase()
}

  1. 图片懒加载工具v-lazy

具体详情可参考:组件篇-手撕lazyLoad和v-lazy

// 引入默认图片
import loadingImg from '@/assets/loading.gif'
let timer = null

// 创建一个监听器
const observer = new IntersectionObserver((entries) => {
  // entries是所有被监听对象的集合
  entries.forEach(entry => {
    // 当被监听元素到临界值且未加载图片时触发。
    if (entry.isIntersecting || entry.intersectionRatio > 0) {
      if (!entry.target.isLoaded) {
        const lazyImage = entry.target
        // 设置img的真实图片地址data-src
        lazyImage.src = lazyImage.dataSrc
        observer.unobserve(lazyImage)
      }
    }
  })
})

export default {
  // inserted时元素已经插入页面,能够直接获取到dom元素的位置信息
  inserted(el, binding, vnode) {
    clearTimeout(timer)
    // 初始化时展示默认图片
    el.src = loadingImg
    // 将需要加载的图片地址绑定在dom上
    el.dataSrc = binding.value

    observer.observe(el)
    // 在组件卸载的时候停止监听
    const vm = vnode.context
    timer = setTimeout(() => {
      vm.$on('hook:beforeDestroy', () => {
        observer.disconnect()
      })
    }, 20)
  },
  // 图片更新触发
  update(el, binding) {
    el.isLoaded = false
    el.dataSrc = binding.value
  }
}

最后-项目github地址

[Vue多页面项目h5]活动页模板传送门github

自此:完成了上述修改后,以后在项目中添加活动页面,都只需要在pages下新增对应目录即可,使用jenkins集成的工具,只需要新增配置参数即可,绝对是一劳永逸的做法。

PS:欢迎交流学习,不足之处,尽请指出。

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 大俊_ 原文链接:https://juejin.im/post/6869244807844364295

回到顶部