学习总结篇,以能否造轮子衡量学习效果。最近一直在做活动页面(+++)o(╥﹏╥)o
[Vue多页面项目h5]活动页模板传送门github
背景
大多数情况下,我们使用 webpack来打包单页应用程序,这个时候只需要配置一个入口,一个模板文件,但也不尽是如此,有时候也会碰到多页面的项目,比如一些生命周期较短的活动页:
活动页需求
移动端h5经常遇到的情况就是,活动多,迭代快。但单页面的情况显然会不适用,主要体现在以下几个方面:
- 重复建立项目,手动😭添加,效率低。
--project1
---node_modules
---src
----style
----common
----components
----views
---package.json
--project2
---node_modules
---src
----style
----common
----components
----views
-
我司的活动页h5有大量的共性:
- 页面自适应
- 大量基础components(如:loading,dialog,btn,海报制作等)
- 大部分主题一致
-
新活动页上线部署需要重复配置jenkins,效率低
基于以上的现状,此时多页面的优势就显现出来了,下面谈谈,经过多页面整合后vue-multi-page
的效果
vue-multi-page使用说明
活动页h5多页面配置,并结合jenkins
和nginx
实现,每次增加一个页面,只需要在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
,则还可以借助jenkins
的Choice Parameter
- Choice Parameter增加选项
page1
page2
- 构建的时候直接选择相应页面即可。
3. 访问地址
http://www.bai.cn/page1/#/home
http://www.bai.cn/page2/#/home
4. 内置环境变量
标识 | 描述 |
---|---|
DEV | 开发环境 |
TEST | 测试环境 |
PRE | 预发布环境 |
PROD | 生产环境 |
5. vue-cli参考链接
vue-multi-page配置说明
1. entry入口配置
vue-cli
的page
属性让我们能定义多个入口文件,借助这个属性,实现活动页多页面的整合。
每个页面都是 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-env
和DefinePlugin
,通过注入环境变量的方式进行环境配置。完成后只会有一个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.js
的chainWebpack
中注入环境变量
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. 其它优化配置
- 抛弃 DLL,选择
hard-source-webpack-plugin
从vue-cli
和create-react-app
中可以知道并没有实用dll
,是因为:Webpack 4
的打包性能足够好的,dll继续维护的必要了。
更好的代替者DLL,选择hard-source-webpack-plugin
chainWebpack: config => {
// 启用缓存
config.plugin('hardSource')
.use(new HardSourceWebpackPlugin())
}
- 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,
}
项目工具类
- 各端判断工具
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()
}
- 图片懒加载工具
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