记一次懒癌发作引发的代码重构:动态加载模块

1,432

前言

这是一个过程曲折、结局温暖的动人故事。
知识点:ES6 import()动态加载模块和webpack的require.context。

先看下重构前的代码

// mock/index.js
import getProvinceCity from './data/getProvinceCity.js'
import getAcListInfo from './data/getAcListInfo.js'
import sendFreezeApply from './data/sendFreezeApply.js'
import getFreezeList from './data/getFreezeList.js'
import getProjectInfo from './data/getProjectInfo.js'
import getProjectList from './data/getProjectList.js'
import getGuaranteeType from './data/getGuaranteeType.js'
import getToken from './data/getToken.js'
import checkProjectUser from './data/checkProjectUser.js'
import checkAiBinding from './data/checkAiBinding.js'
import getAcctType from './data/getAcctType.js'
import getVeriCode from './data/getVeriCode.js'
import getJsApi from './data/getJsApi.js'
import getWeixinUser from './data/getWeixinUser.js'
import getDeductionList from './data/getDeductionList.js'
import getGuaranteeFreezeInfo from './data/getGuaranteeFreezeInfo.js'
import checkCard from './data/checkCard.js'
import approvePay from './data/approvePay.js'
import checkPwd from './data/checkPwd.js'

Mock.mock(/getProvinceCity$/, getProvinceCity)
Mock.mock(/getAcListInfo$/, getAcListInfo)
Mock.mock(/sendFreezeApply$/, sendFreezeApply)
Mock.mock(/getFreezeList$/, getFreezeList)
Mock.mock(/getProjectInfo$/, getProjectInfo)
Mock.mock(/getProjectList$/, getProjectList)
Mock.mock(/getGuaranteeType$/, getGuaranteeType)
Mock.mock(/getToken$/, getToken)
Mock.mock(/checkProjectUser$/, checkProjectUser)
Mock.mock(/checkAiBinding$/, checkAiBinding)
Mock.mock(/getAcctType$/, getAcctType)
Mock.mock(/getVeriCode$/, getVeriCode)
Mock.mock(/getJsApi$/, getJsApi)
Mock.mock(/getWeixinUser$/, getWeixinUser)
Mock.mock(/getDeductionList$/, getDeductionList)
Mock.mock(/getGuaranteeFreezeInfo$/, getGuaranteeFreezeInfo)
Mock.mock(/checkCard$/, checkCard)
Mock.mock(/approvePay$/, approvePay)
Mock.mock(/checkPwd$/, checkPwd)

就是对mockjs最基础的应用:引入静态数据、mock匹配请求路径、返回对应数据。
没一点毛病!除了太普通显得有点low。
low不是错,让我不能忍的是每次新增操作都要CV好几次(最好情况需要复制3次粘贴6次),很累,感觉身体被掏空。
重构势在必行!

for循环走起

不难看出,import和mock都是重复的,先无脑走个for循环。
下面是改造后的代码:

// mock/index.js
const urls = [
  'getProvinceCity',
  'getAcListInfo',
  'sendFreezeApply',
  'getFreezeList',
  'getProjectInfo',
  'getProjectList',
  'getGuaranteeType',
  'getToken',
  'checkProjectUser',
  'checkAiBinding',
  'getAcctType',
  'getVeriCode',
  'getJsApi',
  'getWeixinUser',
  'getDeductionList',
  'getGuaranteeFreezeInfo',
  'checkCard',
  'approvePay',
  'checkPwd'
]

for (let i = 0; i < urls.length; i++) {
  const url = urls[i]
  import data from `./data/${url}.js` // eslint 报错
  Mock.mock(new RegExp(`${url}$`), data)
}

这样以后再有新增,CV一次就好!!!舒服……
可惜,还没试运行,eslint就给报警了:

Parsing error: 'import' and 'export' may only appear at the top level

于是特意回去翻了ECMAScript 6 入门 Module那一章,人说的很明白:

由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

引擎处理import语句是在编译时,这时不会去分析或执行if语句,所以import语句放在if代码块之中毫无意义,因此会报句法错误,而不是执行时错误。也就是说,import和export命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)

此路不通,好在后面有解决方案:

ES2020提案 引入import()函数,支持动态加载模块。

使用import函数动态加载模块

这个简单,动态加载模块走起。

// mock/index.js
for (let i = 0; i < urls.length; i++) {
  const url = urls[i]
  // 动态加载模块
  import(`./data/${url}.js`).then(data => {
    Mock.mock(new RegExp(`${url}$`), data.default)
  })
}

完美!!!不报错了……
欢天喜地的打开页面一看……一直转圈圈。点开network,果然是没拦截住,有两个接口被发送出去了。
又点了其他页面,好的?!……除了这两个接口,其他都是能正常被mock的……
第一时间怀疑是拼写错误,再三比对确认后,拼写无误!
随后在页面又试了几次,发现个规律:只有这两个接口没被mock,且它们都是在created中被调用的。
此时‘js执行机制’、‘异步逻辑处理’的知识点纷至沓来,确定是异步问题无疑了。
赶紧看下main.js:

// main.js
import './mock/index.js' 

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store,
  template: '<App/>',
  components: { App }
})

// mock/index.js
// 动态加载模块
import(`./data/${url}.js`).then(data => {
  Mock.mock(new RegExp(`${url}$`), data.default)
})

不难看出,mock/index.js里面动态加载模块是promise,mock会进异步队列,然后就被排在vue实例化之后执行了。
真相大白!vue实例化过程中(created)调用接口时,mock还没执行,也就自然不会拦截请求了……

解决动态加载模块造成的异步问题

两个方案:Vue实例化延迟执行、mock/index.js模块Promise化。我选择了前者……

// main.js
const useMock = true // 是否使用MOCK
if (useMock && process.env.NODE_ENV === 'development') {
  import('./mock/index.js')
}

// 由于mock中是动态加载的模块,所以当开启mock时需要延迟实例化
setTimeout(() => {
  /* eslint-disable no-new */
  new Vue({
    el: '#app',
    router,
    store,
    template: '<App/>',
    components: { App }
  })
}, useMock ? 100 : 0)

使用require.context实现免配置

经评论区蛋蛋大神的指点,使用webpack的require.context读取文件列表,达成了免配置效果!
以后有新增的话只在data目录下添加js文件就好……
最终代码:

// mock/index.js
import Mock from 'mockjs'

const dataFilesContexts = require.context('./data', false, /\.js$/)
const urls = dataFilesContexts.keys().map(item => {
  //从uri中匹配出文件名
  const fileNameArr = item.match(/(?<=\/).*(?=.js)/) || []
  return fileNameArr[0]
})

for (let i = 0; i < urls.length; i++) {
  const url = urls[i]
  // 动态加载模块
  import(`./data/${url}.js`).then(data => {
    Mock.mock(new RegExp(`${url}$`), data.default)
  })
}

写在最后

把需要“复制3次粘贴6次”的操作降到了零次,这一刻,心中的某种情感被唤醒了。
好似一阵风,吹得头上“CV程序员”的帽子晃了一晃。
我伸手按住,抬起头,开心的像个孩子……