你越是认真生活,你的生活就会越美好——弗兰克·劳埃德·莱特
《人生果实》经典语录

带表达式的 require 语句

webpack官网 依赖管理

如果你的 request 含有表达式(expressions),就会创建一个上下文 (context),因为webpack在编译时(compile time)并不清楚具体导入哪个模块。

示例,考虑到我们有包含 .ejs 文件的如下目录结构:

example_directory
│
└───template
│   │   table.ejs
│   │   table-row.ejs
│   │
│   └───directory
│       │   another.ejs

当下面的require()调用被评估解析:

require('./template/' + name + '.ejs');

webpack 解析require()调用,然后提取出如下一些信息:

Directory: ./template
Regular expression: /^.*\.ejs$/

意思是: 在当前文件下的 template 文件下,匹配以 .ejs 为结尾的文件

不熟悉正则的同学,可以看看下面的解析
正则解析:

/^.*\.(jpg|gif|png|bmp)$/i
  • ^: 匹配字符串的开始位置
  • .*: .匹配任意字符,*匹配数量0到正无穷
  • \.: 斜杠用来转义,\.匹配.
  • (jpg|gif|png|bmp): 匹配 jpg 或 gif 或 png 或 bmp
  • $: 匹配字符串的结束位置
  • i: 不区分大小写。

合起来就是匹配以 .jpg.GIF 或 … 结尾的任意字符串,不区分大小写

context module

会生成一个 context module (上下文模块)。它包含目录下的所有模块的引用,如果一个request符合正则表达式,就能require进来。

context module包含一个 map(映射)对象,会把 requests 翻译成对应的模块 id。

示例 map(映射):

{
  "./table.ejs": 42,
  "./table-row.ejs": 43,
  "./directory/another.ejs": 44
}

此 context module 还包含一些访问这个 map 对象的 runtime 逻辑。

这意味着 webpack 能够支持动态地 require,但会导致所有可能用到的模块都包含在bundle中。

require.context

我们可以通过 require.context() 函数来创建自己的 context

可以给这个函数传入三个参数:

  • 要搜索的目录
  • 标记表示是否还搜索其子目录
  • 匹配文件的正则表达式

webpack 会在构建中解析代码中的 require.context()

语法如下:

require.context(
  directory,
  (useSubdirectories = true),
  (regExp = /^\.\/.*$/),
  (mode = 'sync')
);

示例:

require.context('./test', false, /\.test\.js$/);
//(创建出)一个 context,其中文件来自 test 目录,request 以 `.test.js` 结尾。
require.context('../', true, /\.stories\.js$/);
// (创建出)一个 context,其中所有文件都来自父文件夹及其所有子级文件夹,request 以 `.stories.js` 结尾。

ps:
传递给 require.context 的参数必须是字面量(literal)!

context module API (resolve(), keys(), id)

一个context module会导出一个(require)函数,此函数可以接收一个参数:request

此导出函数有三个属性:resolve, keys, id

  • resolve 是一个函数,它返回 request 被解析后得到的模块 id。
  • keys 也是一个函数,它返回一个数组,由所有可能被此 context module处理的请求(译者注:参考下面第二段代码中的 key)组成。

如果想引入一个文件夹下面的所有文件,或者引入能匹配一个正则表达式的所有文件,这个功能就会很有帮助,例如:

function importAll(r) {
  r.keys().forEach(r);
}

importAll(require.context('../components/', true, /\.js$/));
const cache = {};

function importAll(r) {
  r.keys().forEach((key) => (cache[key] = r(key)));
}

importAll(require.context('../components/', true, /\.js$/));
// 在构建时(build-time),所有被 require 的模块都会被填充到 cache 对象中
  • id 是context module的模块 id 它可能在你使用 module.hot.accept 时会用到。

项目中应用

vue项目动态导入全局组件

目录结构如下
在这里插入图片描述
src\components\common\index.js

export default {
  install(Vue) {
    const ctxRequire = require.context('@/components/common', false, /\.vue$/)
    debugger
    console.log('ctxRequire:', ctxRequire)
    // console.log('ctxRequire.resolve():', ctxRequire.resolve())
    console.log('ctxRequire.keys:', ctxRequire.keys())
    console.log('ctxRequire.id:', ctxRequire.id)
    ctxRequire.keys().forEach((filePath) => {
      const fileName = filePath.replace(/(.*\/)*([^.]+).vue$/ig, '$2') // 获取Vue组件文件名
      // "./breadcrumb.vue".replace(/(.*\/)*([^.]+).vue$/ig, '$2') 返回breadcrumb
      Vue.component(fileName, ctxRequire(filePath).default || ctxRequire(filePath))
    })
  }
}

打印如下
在这里插入图片描述
控制台点击ctxRequire打印的内容,会跳到源码页面,源码如下

var map = {
	"./VTitle.vue": "./src/components/common/VTitle.vue",
	"./breadcrumb.vue": "./src/components/common/breadcrumb.vue",
	"./common-title.vue": "./src/components/common/common-title.vue",
	"./custom-switch.vue": "./src/components/common/custom-switch.vue",
	"./file-list.vue": "./src/components/common/file-list.vue",
	"./more-button.vue": "./src/components/common/more-button.vue",
	"./nodata.vue": "./src/components/common/nodata.vue",
	"./pagination.vue": "./src/components/common/pagination.vue",
	"./previewDialog.vue": "./src/components/common/previewDialog.vue",
	"./server-type.vue": "./src/components/common/server-type.vue",
	"./step-nav.vue": "./src/components/common/step-nav.vue",
	"./svg-icon.vue": "./src/components/common/svg-icon.vue",
	"./tags.vue": "./src/components/common/tags.vue",
	"./upload-img.vue": "./src/components/common/upload-img.vue",
	"./upload-imgs.vue": "./src/components/common/upload-imgs.vue"
};


function webpackContext(req) {
	var id = webpackContextResolve(req);
	return __webpack_require__(id);
}
function webpackContextResolve(req) {
	if(!__webpack_require__.o(map, req)) {
		var e = new Error("Cannot find module '" + req + "'");
		e.code = 'MODULE_NOT_FOUND';
		throw e;
	}
	return map[req];
}
webpackContext.keys = function webpackContextKeys() {
	return Object.keys(map);
};
webpackContext.resolve = webpackContextResolve;
module.exports = webpackContext;
webpackContext.id = "./src/components/common sync \\.vue$";

src\plugins\common.js

import Vue from 'vue'
import commonComps from '../components/common/index'

Vue.use(commonComps) // 会执行commonComps的install方法

src\main.js

import './plugins/common'
...

这样后,就可以在Vue任意地方用这些全局组件,不需要另外注册

PS:
ctxRequire(filePath).default || ctxRequire(filePath)相当于import进来的模块,假如filePath的值是"./breadcrumb.vue"
等同于
import breadcrumb from './breadcrumb.vue'

import breadcrumb from './breadcrumb.vue'

export default {
  install(Vue) {
    const ctxRequire = require.context('@/components/common', false, /\.vue$/)
    ctxRequire.keys().forEach((filePath) => {
      const fileName = filePath.replace(/(.*\/)*([^.]+).vue$/ig, '$2') // 获取Vue组件文件名
      if (fileName === 'breadcrumb') {
        console.log('breadcrumb:', breadcrumb)
        console.log(ctxRequire(filePath).default || ctxRequire(filePath))
        console.log(breadcrumb === (ctxRequire(filePath).default || ctxRequire(filePath))) // true
      }
      console.log('ctxRequire(filePath):', ctxRequire(filePath))
      Vue.component(fileName, ctxRequire(filePath).default || ctxRequire(filePath))
    })
  }
}

打印如下
在这里插入图片描述

Vue 项目 api 请求封装

按不同功能模块放相关的请求,然后通过 require.context() 动态获取,最终在 .vue 文件中直接通过 this.$api.apiName(params) 调用,这样就不用哪个组件需要用,就在哪个组件单独导入。

目录结构
在这里插入图片描述

比如src/utils/api-config/account-admin.js

// 后台账号管理
export const getCountList = {
  url: '/v1/api/admin/operators/getList',
  method: 'get',
  params: {}
}
// 在 .vue 文件里通过 this.$api.getCountList(params) 请求
// params 是一个 json 对象

// 超级管理员创建运营人员
export const createOperator = {
  url: '/v1/api/admin/operators/create',
  method: 'post',
  params: {}
}
// 在 .vue 文件里通过 this.$api.createOperator(params) 请求
// params 是一个 json 对象

// 冻结运营人员
export const setPersonFrozen = {
  url: '/v1/api/admin/operators/frozenAccount/{id}/{id}',
  method: 'put',
  params: {}
}
// 在 .vue 文件里通过 this.$api.setPersonFrozen(id, id) 请求

// 更新状态
export const updateUserStatus = {
  url: '/v1/api/admin/user/updateStatus',
  method: 'put',
  params: {},
  paramsType: 'query' // 数据参数跟get请求一样
}
// 在 .vue 文件里通过 this.$api.createOperator(params) 请求
// params 是一个 json 对象

src\utils\api-config\index.js

// 获取除 index.js 外 其他以 .js 结尾的文件
const ctx = require.context('../api-config', false, /(?<!index)\.js$/)
const apiConfig = {}

// 合并所有模块export api
// let res = []
ctx.keys().forEach((item) => {
  Object.keys(ctx(item)).forEach((apiFnName) => {
    if (Object.keys(apiConfig).includes(apiFnName)) throw Error(`api重复定义:${apiFnName}`)
    Object.assign(apiConfig, { [apiFnName]: ctx(item)[apiFnName] })
  })
})
console.log('apiConfig:', apiConfig)
export default apiConfig

在这里插入图片描述
src\plugins\axios.js

import Vue from 'vue'
import {
  Store
} from 'vuex'
import axios from 'axios'
import apiConfig from 'utils/api-config/index'
import VueCookies from 'vue-cookies'

// axios.defaults.baseURL = process.env.baseUrl
axios.defaults.timeout = 5000

axios.interceptors.response.use((response) => {
  if (response.status !== 200) return response.data
}, (error) => {
  // 超时处理
  if (error.message && error.message.includes('timeout')) throw new Error(Vue.prototype.$message
    .error({
      message: '请求超时, 请重试~',
      center: true
    }))
  if (error.response && error.response.status === 401) {
    // toLogin()
    VueCookies.remove('aupup_login_user')
    location.replace('/login')
    return Promise.reject(error)
  }
  // 一般错误提示
  Vue.prototype.$message.error({
    message: error.response.data.message || '网络错误,请稍后再试',
    center: true
  })
  return Promise.reject(error)
})

Store.prototype.$api = Vue.prototype.$api = {}
Object.keys(apiConfig).forEach((item) => {
  let curItem = apiConfig[item]
  Vue.prototype.$api[item] = function (...args) {
    let tempUrl = curItem.url
    let params = args.find((p) => typeof (p) === 'object') || {}
    let ids = args.filter((ns) => ['string', 'number'].includes(typeof (ns)))
    if (ids.length && tempUrl.match(/{(.+)}/)) ids.forEach((id) => {
      if (!tempUrl.match(/{(.+?)}/)) return console.warn(`api函数多余参数传递: ${item}`)
      let replaceId = tempUrl.match(/{(.+?)}/)[0]
      tempUrl = tempUrl.replace(replaceId, id)
    })
    if (tempUrl.match(/{(.+?)}/)) console.warn(`api缺少参数传递: ${tempUrl}`)
    if (['get', 'delete', 'put'].includes(curItem.method)) params = {
      params
    }
    if (curItem.method === 'put' && curItem.paramsType === 'query') {
      const {
        params: cParams
      } = params
      Object.keys(cParams).forEach((key) => {
        tempUrl += `${tempUrl.includes('?') ? '&' : '?'}${key}=${cParams[key]}`
      })
    }
    if (curItem.method === 'put' && params.params) params = params.params
    return axios[curItem.method](`${tempUrl}`, params, {
      headers: curItem.headers || {}
    })
  }
})

src\main.js

...
import './plugins/axios'
...

动态导入路由

类似 nuxt 框架,根据文件夹名称位置,自动生成对应的路由,这样可以不用新开一个路由,就去手动新增一次路由配置

Vue源码学习目录

Vue源码学习完整目录


谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强

Logo

前往低代码交流专区

更多推荐