一文读懂 require.context ,实现前端工程自动化
一文读懂require.context,实现前端工程自动化带表达式的 require 语句context modulerequire.context示例:context module API (resolve(), keys(), id)vue项目动态导入全局组件Vue源码学习目录你越是认真生活,你的生活就会越美好——弗兰克·劳埃德·莱特《人生果实》经典语录带表达式的 require 语句webp
一文读懂require.context,实现前端工程自动化
你越是认真生活,你的生活就会越美好
——弗兰克·劳埃德·莱特
《人生果实》经典语录
带表达式的 require 语句
如果你的 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源码学习目录
谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强
更多推荐
所有评论(0)