Vue SPA应用模块化打包实践

1,770 阅读8分钟

目标:对SPA应用进行模块化打包

开发环境下:实现实现各个模块可以独立启动,不同的模块可以组合启动 

构建环境下:实现各个模块独立打包(即改动哪个模块就打包那个模块),可以一条命令打包所有模块  

基础环境版本:  

  node:v8.11.3、vue: 2.8.2、webpack: 3.6.0

构建开始

首先下载一个官方的脚手架模版,开始我们的工程,构建我们的项目目录,如下图



这里我建了三个模块,分别为moduleA,moduleB,moduleC。router/store/views文件夹分别对应着路由/状态管理库/模块页面,每个文件夹下面都有一个index文件,对应着每个模块路由/store/pages集成的地方,当然网上也有很多人喜欢每个模块的文件夹下面放着每个模块的路由啥的,咱们这里这样的构建 个人感觉在import引入文件的时候以及视觉效果更好一些,下面看一下每个模块里面的构造以moduleA为例, 如下图:

views部分


entry.js是我们每个模块的入口,pages文件夹里面放着每个模块的页面,当然全局的一些公共的组件啥的我们放到外层的common里,大家根据自己的需求去设置,views下面的index.vue是我们系统的入口文件,他的构造是这样的:



路由部分



这里是路由的示例,每个模块下有一个主路由,外加他的子路由,我们这里将子路由拆分出来单独成一个模块,方便维护以及其他模块的引入,其他(moduleB,moduleC)的路由模块与此相同的构建

store部分

这里与路由的构造类似, 如下图:

这里是我们的stroe->moduleA->children.js部分

const storeDetail = {  
  state:{    
    name:'storeA' 
  },  
  getters:{},  
  mutations:{},  
  actions:{},
}  
export { storeDetail }

这里是我们的stroe->moduleA->index.js部分

import Vue from 'vue'  
import Vuex from 'vuex'  
import {storeDetail} from "./children";  
import { storeDetail as rootStore } from '../rootChildren'  

Vue.use(Vuex)
const store = new Vuex.Store({  
  namespaced: true, 
  ...rootStore,  
  modules:{ 
     moduleA: storeDetail 
  }
})
export default store

这里我设置了每个模块下children.js导出的名字相同,为啥这么做,后期如果循环引入的话,可以更加方便的取出每个模块,再一个就是尝试下import...as,提醒大家我们还有引入的模块设置别名这个操作,有种高大上的赶脚(哈哈,这条可以忽略~~~),其他模块下的构造与此相同,

router/stroe文件夹下面的index.js是我们用来集成各个模块的路由文件chiildren.js,最后将引入到src的main.js里面

router->index.js

import Vue from 'vue'  
import Router from 'vue-router'
Vue.use(Router)

//  import()动态引入方式探究,根据传参动态引入?
import { moduleChildren as moduleA }  from './moduleA/children'
import { moduleChildren as moduleB }  from './moduleB/children'
import { moduleChildren as moduleC }  from './moduleC/children'  
const combinationRouter = { 
       moduleA, 
       moduleB,        
       moduleC  
}
const combinationRouterArr = []
const container = () => import('@/views')  
const error = () => import('@/components/error')

//  获取启动参数
let moduleName = process.env.ENTRY_NAME;  
let moduleNameType = process.env.ENTRY_NAME_TYPE;  
let moduleNameArr = moduleNameType === 'string' ? moduleName.split(',') :  Object.keys(combinationRouter)
    moduleNameArr.map(item => {
         combinationRouterArr.push(...combinationRouter[item])    
   })
export default new Router({
  routes: [ 
   {      
     path: '/',      
     name: 'container',      
     component: container,
     redirect: '/moduleA/home',
     children: combinationRouterArr    
  },{      
     path: '*',
     name: 'error', 
     component: error    
  }  
 ]
})

store->index.js

import Vue from 'vue'  
import Vuex from 'vuex'  
// 导入模块路由  
import { storeDetail as rootStore} from './rootChildren'  
import { storeDetail as moduleA} from './moduleA/children'  
import { storeDetail as moduleB} from './moduleB/children'  
import { storeDetail as moduleC} from './moduleC/children'  

Vue.use(Vuex)  
const combinationStore = {  
   moduleA,  
   moduleB,  
   moduleC
}
const combinationStoreObj = {}  
//  获取启动参数  
let moduleName = process.env.ENTRY_NAME;  
let moduleNameType = process.env.ENTRY_NAME_TYPE;  
let moduleNameArr = moduleNameType === 'string' ? moduleName.split(',') :  Object.keys(combinationStore)    
    moduleNameArr.map(item => {  
      combinationStoreObj[item] = combinationStore[item]    
   })
  
const store = new Vuex.Store({ 
   ...rootStore,  
  modules: {  
    ...combinationStoreObj  
  }  
})
export default store

这里store下面还有一个rootChilren.js这个是将store里面state/getters/mutations/actions独立出来方便每个模块去引入,当然你也可以继续将vuex去拆分成4个文件,然后再集成,这样更加有利于后期的维护,

router->index.js下有这样的一段代码

//  获取启动参数
let moduleName = process.env.ENTRY_NAME;  
let moduleNameType = process.env.ENTRY_NAME_TYPE;  
let moduleNameArr = moduleNameType === 'string' ? moduleName.split(',') :  Object.keys(combinationRouter)
    moduleNameArr.map(item => { 
        combinationRouterArr.push(...combinationRouter[item])    
   })

这里我们是根据传入进来的参数去动态组合启动的模块,stroe下面的组合与此类似,当然这里我是现将所有模块的路由导入组装成一个对象,然后再根据启动参数去组装启动,这种半自动的方式赶脚有点不爽,这里厉害的童鞋可以尝试下import().then()动态引入的方式(这里有个问题就是,这种import()的方式是异步的,要这样做需要在循环里面动态引入,将获取的参数进行组合,异步变同步...原谅我没有实现~~~)

看到这有人可能会问这两个启动参数是怎么设置获取的

let moduleName = process.env.ENTRY_NAME;
let moduleNameType = process.env.ENTRY_NAME_TYPE;

是这样的,我们的启动命令是这样的(示例) npm run dev --entryname=moduleA

这样你可能会问 为啥不是 npm run dev moduleA ,

我也想这样方便写,但是一这样写就会报错(具体什么原因我也没查到)但是这样写总比在启动脚本里写 “dev:moduleA”: ............,方便很多

接下来就是获取这个启动参数,process.env.npm_config_entryname就是用它来获取,当然你也可以不用entryname,这个名字根据需要自己定义,感兴趣的童鞋可以去了解下node的process,这里不做介绍,

接下来在我们的config->dev.env.js下做如下配置

'use strict'  
const merge = require('webpack-merge')  
const prodEnv = require('./prod.env')  
const type = typeof (process.env.npm_config_entryname)  
module.exports = merge(prodEnv, {   
   NODE_ENV: '"development"',  
   ENTRY_NAME: `"${process.env.npm_config_entryname}"`,  
  ENTRY_NAME_TYPE: `"${type}"`
})

我们在原始文件的基础上加了两个字段,就是我们要获取的那两个参数,这里注意下写法,反引号里面的变量要加引号,不然会报错,这样就可以在router/store的index.js文件里获取到这两个参数,去进行启动了,

我们的启动参数要用英文的逗号隔开,这里也可以用别的符号进行分隔,但是获取的时候可能不会带着分隔符,有兴趣可以试一下

接下来就是设置我们的入口文件了

在build文件夹下面新建handle.entry.js用来处入口

// 在入口文件处解析ES6语法  
require ('babel-polyfill')  
//  输入空  打包全部,输入对应参数打包相应的模块  
const glob = require('glob')  
//  获取启动环境  
const startUpEnvir = process.env.NODE_ENV  
// 测试环境  
function devFun () {  
  let name = process.env.npm_config_entryname   
  let entryPathDev = {}  
  let url = (!name || name.split(',').length > 1) ? './src/main.js' : `./src/views/${name}/entry.js` 
      entryPathDev.web = ['babel-polyfill', url] 
    return entryPathDev  
}  
// 生产环境
function proFun () {  
   let moduleSrcArray = []  
   let entryPathPro = {}  //配置入口文件  
   let [,, name] = process.argv    
    moduleSrcArray = name ?  name.split(',') : getAllEntry()  
    moduleSrcArray.map(item => {  
       entryPathPro[item] = ['babel-polyfill', `./src/views/${item}/entry.js`]   
    })   
   return entryPathPro}   
  // 获取所有打包模块  
  function getAllEntry (){  
    let arr = []  
    let moduleFile = glob.sync('./src/views/*/entry.js')
       moduleFile.map(item => {   
         arr.push((item.split('/'))[3])   
       })   
    return arr  
   }   

exports.handleEntryPath = function (name) {
 
  let entryPath = startUpEnvir === 'production' ?  proFun() :  devFun()      
  return entryPath
}

这里是测试与生产环境打包入口的处理部分,核心就是根据参数去设置一个入口还是多个入口,要装一下glob,方便我们进行文件检索

npm i glob -S

之后在webpack.base.conf.js引入使用


命令行输入:

 npm run dev 就会启动所有模块 ,

npm run dev --entryname=moduleA,moduleB就会启动A,B两个模块,

PS:启动参数可不要乱输入哦,我这里没做太多的参数检查,不按规则出牌,有可能会报错~~

下面看下启动效果:


到这里我们的测试启动部分已经完成了,接下来就是打包构建部分的实践了

打包部分:

config->index.js做如下设置


这里又出来一个知识点那就是 process.argv ,就是用来获取build参数的,他返回的是一个数组 

总共1个模块,正在打包模块moduleA--> moduleA
argv=== 
[ 
  '/usr/local/bin/node',
  '/Users/quanhudemac/Desktop/mutiModule/build/build.js',
  'moduleA,moduleB' 
]

从数组第三个元素开始就是我们输入的参数,参数以空格作为分割,大家可以试下输入参数的打印情况

build文件下面新建build.pro.js,输入以下代码

process.env.NODE_ENV = 'production'  
const path = require('path')  
const execFileSync = require('child_process').execFileSync;  
const buildFile = path.join(__dirname, 'build.js')  
const t = require('./handle.entry').handleEntryPath()
let itemObj = Object.keys(t)
let len = itemObj.length  

for( const module of itemObj){  
  console.log(`总共${len}个模块,正在打包模块${module}-->`,module)  
  let res = execFileSync( 'node', [buildFile, module], {})  
  process.stdout.write(res + '\n\n');  
}

这里我们用到了node的child_process子线程进行多模块循环打包

execFileSync( 'node', [buildFile, module], {})

上面这段代码不懂的可以看下node的child_process的api,如果看不懂可以process.stdout.write()打印下在命令行看到打印结果,你可能就会明白了

let res = execFileSync( 'node', [buildFile, module], {})  
  process.stdout.write(res + '\n\n');

build.js文件里面输入

'use strict'  
require('./check-versions')()
process.env.NODE_ENV = 'production'

console.log('argv===', process.argv)  
const ora = require('ora')  
const rm = require('rimraf')  
const path = require('path')  
const chalk = require('chalk')  
const webpack = require('webpack')  
const config = require('../config')  
const webpackConfig = require('./webpack.prod.conf')  
const spinner = ora('打包进行中,请稍等哒~')  
spinner.start()
...........

这是我打包moduleA的打印结果


最后改一下我们的package.json文件,将构建的文件改成build.pro.js

"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",    
    "start": "npm run dev",    
    "unit": "jest --config test/unit/jest.conf.js --coverage",    
    "e2e": "node test/e2e/runner.js",   
    "test": "npm run unit && npm run e2e",    
    "build": "node build/build.pro.js"  
}

接下来打包实验下结果

tip:打包的时候我们不用像测试环境一样加上--entryname, 可以直接跟参数

npm run build moduleA,moduleB


这样A,B模块就被打包出来了,如果你想打包所有模块 直接输入npm run build即可

这里有几个地方大家可以优化下,

iview我是直接安装的,每次打包的时候都会把它打包进去

大家使用的时候可以改成cdn的方式引入,

在一个就是babel-polyfill这东西,打包的时候也很耗时,所以建议大家改成按需引入,或者配置下.babelrc文件

可能会有人想问,如果我想将两个模块进行组合打包到一起怎么办,这里我的建议是在项目目录下新建一个文件夹,专门用进行组合模块打包,每次需要打包到一起那两个模块,就在这里面进行配置下,

前面有提到import()函数动态引入router/store的方式,如果大家有好的办法,欢迎留言,

代码中可能有不足的地方,欢迎大家指正,

源代码已传至Github,地址:github.com/watersea/mu…,如果你感觉不错的话麻烦给个star,

参考文章:segmentfault.com/a/119000000…