require.context()简介

114 阅读3分钟

背景

在我们项目开发中,经常需要 import 或者 export 各种模块,但随着我们项目越来越大,如果还是通过 import 分别引入文件,那将是一件非常麻烦的事情。

这里先举一个十分常见的例子,比如 vue 项目的 vuex 文件非常多,每次新增一个模块,都需要重新引入一次,以及注册一次,不仅不美观而且很麻烦。怎么可以做到只需要新建 js 完成逻辑,自动就会注入到vuex中呢?

这就说到了今天的主题,使用到了 Webpack 中的 require.context() 。

解析与实现

官网解释:给这个函数传入三个参数:一个要搜索的目录,一个标记表示是否还搜索其子目录, 以及一个匹配文件的正则表达式。
Webpack 会在构建中解析代码中的 require.context() 。
语法如下:

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

示例:

require.context('./test', false, /.test.js$/);
//(创建出)一个 context,其中文件来自 test 目录,request 以 `.test.js` 结尾。

下面来解析一下这个方法的实现。

1. 解析 require.context()

image.png

在解析之前,需要先看一下 store 目录,包含很多模块,childModule
里还有子模块,按正常来写 index.js 里将会引入很多这样的 js ,然后再去 store 注册,就像上面所说的,这是不美观且麻烦的。接下来我们采用上面所说的 require.context ,先在 index.js 里将它打印出来,让我们揭开它的真面目。

//index.js
let context = require.context('./modules', true, /.js$/)
console.log('context', context);

这里会打印出来一个叫 webpackContext 的方法,点击这个webpackContext 方法,可以看到以下内容。

var map = {
	"./moduleA.js": "./src/store/modules/moduleA.js",
	"./moduleB.js": "./src/store/modules/moduleB.js",
	"./childModule/moduleD.js": "./src/store/modules/childModule/moduleD.js",
	...
};


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/store/modules sync recursive \.js$";

代码很容易看懂,require.context 执行后,返回一个方法 webpackContext ,这个方法又返回一个 __webpack_require__ ,这个 __webpack_require__ 就是一个模块加载器,而所有的模块都会以对象的形式被读取加载。同时 webpackContext 还有二个静态方法 keys 与 resolve ,一个 id 属性。

  1. keys:经过打印,可以发现是以引入模块的相对路径为 key 组成的数组。
const context = require.context('./modules', true, /.js$/)
console.log('keys', context.keys());
//['./childModule/moduleE.js', './childModule/moduleF.js', './childModule/moduleG.js', './childModule/moduleH.js', './moduleA.js', './moduleB.js', './moduleC.js', './moduleD.js']
  1. resolve:接受一个参数 request ,request 为 modules 文件夹下面匹配文件的相对路径,也就是上面的 key ,然后返回这个匹配文件相对于整个工程的相对路径。
const context = require.context('./modules', true, /.js$/)
console.log('resolve', context.resolve('./moduleA.js'));
//resolve ./src/store/modules/moduleA.js
  1. id:执行环境的 id ,返回的是一个字符串。

再来打印一下 webpackContext  方法,可以看出来返回了一个模块。

const context = require.context('./modules', true, /.js$/)
console.log('webpackContext', context('./moduleA.js'));
//webpackContext Module {default: {…}, __esModule: true, Symbol(Symbol.toStringTag): 'Module'}

了解了上面的代码,接下来可以就可以进行实践了。

2.实现vuex模块自动化注入

import Vue from 'vue';
import Vuex from 'vuex';
import camelcase from 'camelcase';    //驼峰命名的一个npm包
Vue.use(Vuex);

const context = require.context('./modules', false, /.js$/);
    //获取moudules文件下所有js文件;
const modules = context.keys().reduce((modules, modulePath) => {
  const moduleName = modulePath.replace(/(.*/)*([^.]+).js$/ig,"$2");
  const module = context(modulePath)
  modules[moduleName] = module.default
  return modules
}, {})

export default new Vuex.Store({
    modules
});

通过以上代码就可以实现自动分 module 注册 store 。

其他使用场景

除了上面的vuex的例子以外还可以用到其他地方,在这里简略介绍一下。

1.vue中组件自动化全局注册

在我们的项目中,可能会有很多全局的基础组件,例如buttoninput等。
如果有很多这样的组件,放在入口文件 main.js 中也会很冗长,所以可以把全局组件放在一起,通过 require.context 引入,也省去了每次都往 component 里注册的繁琐。
例如放在 components 文件夹下,简单说一下实现方法。

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst' //首字母大写
import camelCase from 'lodash/camelCase' //驼峰命名
const requireComponent = require.context(
  // 其组件目录的相对路径
  './components',
  // 是否查询其子目录
  false,
  // 匹配基础组件文件名的正则表达式
  /Base[A-Z]\w+.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = requireComponent(fileName)

  // 获取组件的 PascalCase 命名
  const componentName = upperFirst(
    camelCase(
      // 获取和目录深度无关的文件名
      fileName
        .split('/')
        .pop()
        .replace(/.\w+$/, '')
    )
  )
  // 全局注册组件
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})

大家可以直接看官方文档,有很详细的描述。
vue官方文档-基础组件的全局注册

2.路由“去中心化”管理

这个主要是在路由过多时,分模块管理路由,例如下面这样。

// rootRoute.js
const rootRoute = {
  childRoutes: [
    {
      path: '/',
      component: Home,
      childRoutes: [
        require('./modules/home/route'), //首页模块
        require('./modules/class/route'), // 课堂模块
        require('./modules/teacher/route'), // 教师模块
        // ...
        // 其他大量新增模块
        // ...
      ]
    }
  ]
};

这里可以改写成

const rootRoute = {
  childRoutes: [
    {
      path: '/',
      component: Home,
      childRoutes: (r => {
        return r.keys().map(r);
      })(require.context('./', true, /^./modules/((?!/)[\s\S])+/route.js$/))
    }
  ]
};

这样即使新增模块,也不需要考虑其他的事情,专注于模块开发。

# 结尾
希望对大家有所帮助~