webpack4 + vue多页面项目精细构建思路

webpack4 + vue多页面项目精细构建思路

原文链接:https://zhangzippo.github.io/posts/2019/05/12/_20xx-05-10mutilpage.html

虽然当前前端项目多以单页面为主,但多页面也并非一无是处,在一些情况下也是有用武之地的,比如:

  1. 项目庞大,各个业务模块需要解耦
  2. SEO更容易优化
  3. 没有复杂的状态管理问题
  4. 可以实现页面单独上线

前言

这里就第4点做一些解释,也对多页面的应用场景做一个我认为有价值的思路,在组内的一个项目中,因为项目日益膨胀,拆分系统有一定困难,项目页面达到200+个以上, 因此构建速度十分缓慢,部署时间也很长,经常因为文案的更改及一些简单的bug修复就要进行重新构建,如果采用单页面一方面构建部署时间会随着体量增大,另一方面在工程上不好进行拆分。这时候多页面就存在一种优势,我们可以在前端做一个空框架只包含菜单,内容区域采用多页面结构,当我们部署上线时可以只针对单个页面进行上线,速度大幅度提升(单页面内部可以集成前端路由),这样业务模块间也可平滑解耦。

项目架构

vue + typescript + webpack4
vue项目,并没有使用vue-cli,原因是对于开发人员来说,了解构建的详细流程很重要,vue-cli这类工具的目的是快速实现项目的搭建,让开发人员快速接手,快速进入 业务代码编写,因此隐含的为我们做了很多事,很多构建及本地开发的优化等等,但对于开发人员来说了解每个步骤,每个细节是做什么的对自身成长很有帮助(尤其是组里的很多程序员都不爱使用高度封装的东西)。

思路

对于多页面来说,与单页面对比无非就是以下几个问题:

  1. entry入口文件为多个,需要考虑页面多需要自动生成,少的话提前预置几个就可以。
  2. htmlWebpackPlugin使用时也需要相应的添加多个。
  3. 公共静态资源提取的问题,splitchunkplugin是否需要使用的问题。
  4. 最后就是支持项目的部分构建的功能实现

为达到我们的终极目标,也就是能够部分代码进行构建,我们将一个项目从业务角度进行一个划分,两个层级,模块和页面,模块代表一个具体业务场景,页面代表这个业务场景的各个页面,我们将支持进行单/多模块和单/多页面的打包。

开始

首先先看一下我们的项目目录结构
├── build_tasks // 构建脚本
├── config // 配置文件
├── src // 源码路径
└── static //build后文件路径
src目录:
src
├── global.js // 项目全局工具
├── modules // 模块
│ ├── Layout.vue
│ ├── moduleA // 具体模块名
│ │ └── pageA // 具体页面名称
│ │ ├── xx.js
│ │ ├── index.vue

自动生成entry

由于我们的页面非常之多,因此我们肯定是需要自动生成entry文件的,并且这一步是需要在进入webpack构建流程之前就要做好的。我们创建一个build_entries.ts的文件,用于编写创建entry流程,这里放一些核心代码


const getTemplate = pagePath => {
  return (
  `
import App from '${pagePath}';
import Vue from 'vue';
  new Vue({
  render: function (h) {
  return h(App);
}
}).$mount('#app');`);
}
const scriptReg = /<script([\s\S]*?)>/;
/**
 * 判断文件应该采用的后缀
 */
const getSuffix = (source: string): string => {
  const matchArr = source.match(scriptReg) || [];
  if(matchArr[1].includes('ts')){
    return '.ts'
  }
  return '.js';
};

const generateEntries = () => {
  const entries = {};
  /***一些前置代码拿到pages*/
  if (!pages.length) return entries;
  // 清除entries
  rimraf.sync(entryPath+'/*.*');
 
  pages.forEach(page => {
    const relativePage = path.relative(vueRoot, page);
    const source = fs.readFileSync(page, 'utf8');
    const suffix = getSuffix(source);
    const pageEntry = path.resolve(entryPath, relativePage.replace(/\/index\.vue$/, '').replace(/\//g, '.')) + suffix;
    const entryName = path.basename(pageEntry, suffix);
    entries[entryName] = pageEntry
    if (fs.existsSync(pageEntry)) return;
    const pagePath = path.resolve(vueRoot, relativePage);
    const template = getTemplate(pagePath);
    fs.writeFileSync(pageEntry, template, 'utf-8');
  });
  return entries
}

export const getEntriesInfos = ()=>{
  return generateEntries();
}

大概解释下思路,我们规定项目目录结构为modules/xxmodle/xxpage,我们以命名为index.vue的页面为入口页面,为每个index.vue创建入口的js模版(getTemplete方法),生成的entry名称为"模块名.页面名.js"。因为项目内需要支持ts,因此我们还需要判断vue内的script标签的语言,以便创建ts格式的entry还是js格式的entry。
我们的webpack配置:

const entries  = getEntriesInfos();
const common = {
  entry: entries,
  output: {
    filename: `[name]-[hash].bundle.js`,
    path: path.resolve(rootPath, 'static'),
    publicPath,
  },

公共文件提取

因为我们是多页面,每个页面都需要加载核心的包(如vue,element-ui,lodash等等)而这类包我们是不常变化的,因此我们需要使用webpack的dllplugin来剥离他们出来,不参与构建,我们的项目中也可能会有我们自己的全局工具包,这部分代码不适合提取,只需要在entry中再加入一个common的entry即可。对于单页面内是否需要使用splitchunk,在我的实践中是没有使用的,但是这个看情况,如果页面引用的包确实比较大(毕竟vue这类框架包已经被提取出去了,这个概率不大)那么可以使用splitchunk来分离,我目前的实践是合并到一个页面的js内,单页面js在gzip后在200k以内都可忍受。
下面放一下dll的配置
webpack.dll.config.ts

const commonLibs = ['vue','element-ui','moment', 'lodash']

export default {
  mode: 'production',
  entry: {
    commonLibs
  },
  output: {
    path: path.join(__dirname, 'dll_libs'),
    filename: 'dll.[name].[hash:8].min.js',
    library: '[name]',
    // publicPath: '/static/'
  },
  plugins: [
    new CleanWebpackPlugin(),
    new webpack.DllPlugin({
      context: __dirname,
      path: path.join(__dirname, 'dll_libs/', '[name]-manifest.json'),
      name: '[name]'
    }),
    new assetsWebpackPlugin({
      filename: 'dll_assets.json',
      path: path.join(__dirname,'assets/')
    })
  ]
} as webpack.Configuration;

如代码所示我们将'vue','element-ui','moment', 'lodash'这几个组件提取打成一个公共包命名为commonLib,这里使用了assetsWebpackPlugin用于生成一个json文件,记录每次dll构建的文件名(因为每次构建hash是不一样的),为的是在使用webpackhtmlplugin的时候拿到这个结果注入到模版页面中去。
生成的json记录类似:

{"commonLibs":{"js":"dll.commonLibs.51be3e86.min.js"}}

这样我们就可以在webpack配置文件中取到这个名字:

const dllJson = require('./assets/dll_assets.json');
for(let entryKey in entries){
  if(entryKey!== 'global'){
    common.plugins.push(
      new HtmlWebpackPlugin({
        title: allConfiguration[entryKey].title,
        isDebug: process.env.DEBUG,
        filename: `${entryKey}.html`,
        template: 'index.html',
        chunks:['global', entryKey, ],
        chunksSortMode: 'manual',
        dll_common_assets: process.env.NODE_ENV !== 'production'?'./dll_libs/' + dllJson.commonLibs.js : publicPath + 'dll_libs/' + dllJson.commonLibs.js,
      
      }),
    )
  }
}

因为是多页面,因此我们webpackhtml使用时也是要添加多个的,这里根据生成的json拿到dll的文件名注入到模版页面中。

按需打包

接下来我们要支持进行按需构建打包,支持单/多模块以及单/多页面的打包,这里怎么做呢,可以在构建时传入环境变量,然后在build_entry中判断环境变量进行局部打包,因为打包的入口是entry的数量决定的。
命令可以这样构成:

 MODULES=xxx,xxx PAGES=sss,sss npm run build

build_entry相关代码,在generateEntries方法中

const entries = {};
  const buildModules = process.env.MODULES || '*';
  const buildPages = process.env.PAGES || '*';
  const filePaths = `${!buildModules.includes(',') ? buildModules : '{'+buildModules+'}'}/${!buildPages.includes(',') ? buildPages : '{'+buildPages+'}'}/*.vue`
  const pages = glob.sync(path.resolve(vueRoot, filePaths)).filter(file =>{
    return /index\.vue$/.test(file) || [];
  })
  if (!pages.length) return entries;

上面的方法根据传入的环境变量拼对应的页面及模块路径,通过glob的支持生成对应的entyr进行构建。

多页面线上发布

多页面构建完成之后就是发布流程,发布流程其实也会变的简单,如果是单页面每次构建完成都要整体替换静态文件(js,css),多页面模式下,我们只需要替换对应页面的文件即可,一般的思路是页面文件可以上传到部署的服务器,然后静态js,css等文件直接扔到CDN上即可,发布不会影响到其他页面,即便出错也不会影响项目,而且效率极高,这部分代码就不展示了,只是提供思路,毕竟每个项目发布流程都不太一样。

总结

以上是我对多页面应用场景的一个思路,它是有一定的适用场景的,比较适合大而全而且模块划分清晰的系统。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容