工程自动化导入:require.context

2,655 阅读3分钟

强烈建议使用Vite的小伙伴能够阅读到文章末尾

webpack帮我们把项目打包到dist目录下,不会是无脑把所有文件都打包,会先静态解析,把我们需要用的文件打包,比如没有任何地方引用的图片,打包后,这个图片就不会出现在dist/static/img目录内。

1.require

const { stat,exists} = require('./index.js')

require是运行时加载,其实际上是加载整个模块并生成一个对象,然后再从对象中读取我们调用的属性,所以无法做静态优化。上面代码的结果就是相当于

const _fs = require('./index.js')
const stat = _fs.stat
const exists = _fs.exists

传参无法使用变量

require('./image/title.png')

很多时候我们都希望能够将上面这种引入方式,写成动态的

let imagePath = './image/title.png'
require(imagePath)

imagePath只有在运行时才会明确知道需要引入的路径,但是这种写法是不允许的,或许从语法上来说不会报错,但是webpack在构建打包时并不能解析出需要的路径
require需要知道至少能确定是哪个目录

let imageFullName = 'title.png'
1. require(`./image/${imageFullName}`)

let imageFullName = 'title'
2. require(`./image/${imageFullName}.png`)

以上两种方式都是可以的,就算只有一个明确的目录,webpack也还是可以解析的,但是这样会导致生成的上下文模块context会包含该目录下所有可能会用到的模块。

同步&异步加载

异步加载

require(['./mypage.vue'],resolve)

在vue官网中的异步组件板块大部分人应该都看到过这段代码,vue官网在这段代码中有注释这个特殊的'require'语法将会告诉webpack自动将你的构建代码切割成多个包,这些包会通过ajax请求加载
更直观的可以解释为:
require第一个参数为数组时,则为异步加载,然后第二个参数为回调函数,在回调函数中操作加载的模块
require第一个参数为字符串时,则为同步加载,且将无法传递第二个参数。
同步加载

var mypage = require('./mypage.vue')

2.require.context

require直观上不同的是,require.context是引入多个模块
require.context可以理解为创建自己的上下文模块context
有三个参数:
1.directory 文件目录
2.useSubdirectories 是否查找子目录 3.regExp 要匹配文件的正则表达式

// 引入model目录下的所有以js后缀的文件
require.context('./model',true,/.js$/)

require.context可以理解为内部将所有模块整合为一个map,模块以路径为key

{
'./a.js':model,
'./b/f.js':model,
...
}

require.context返回一个函数,接受一个key参数,然后返回对应key的模块
require.context返回的函数有三个属性:
1.resolve 是一个函数,它返回 request 被解析后得到的模块 id。 2.keys 是一个函数,返回一个有所有模块的key组成的数组 3.id 是 context module 里面所包含的模块 id

3.import

import命令是静态引入,与require函数的动态引入不同,在编译阶段就完成了加载,就性能而言要优于require,但同时如果项目中大量的import命令静态引入模块,会导致初始包变大,直接影响的便是加载时长,所以又提出了一个import()函数,其两者的区别主要是在与一个是静态引入,一个是在运行时引入。
因为import()是运行时执行,所以可以放在任何地方,import命令的话,则必须放在模块顶层 import()返回的是一个Promise,即为异步加载

let imageUrl = ''
import('./image/title.png').then(res => {
  imageUrl = res
})

import()可以用来解决很多按需加载的性能优化问题,例如vue路由组件的按需加载、局部组件注册时的异步加载

4.利用require.context完成工程自动化导入

import a from "./a.js"
import b from "./b.js"
import c from "./c.js"
...

工作当中经常会遇到以上场景,像这种脱离业务且重复有规律的操作完全可以想办法弄成自动化
vuex分模块设计来举例

//index.js
import Vue from 'vue'
import Vuex from 'vuex'

import app from "./model/app.js"
import user from "./model/user.js"
...

export default new Vuex.Store({
   modules:{
     app,
     user,
     ...
   }
})

当项目应用很大时,甚至会分十几上百个模块,届时光引模块的代码都会显得十分冗余。可以利用require.context一次性将模块全部引入

//index.js
import Vue from 'vue'
import Vuex from 'vuex'
import { resetModelName } from './util'

// 自动导入model
const requireModels = require.context('./model', true, /.js$/)
const storeModules = {}
requireModels.keys().forEach((item) => {
    storeModules[resetModelName(item)] = requireModels(item).default
})

export default new Vuex.Store({
   modules:{
     ...requireModels
   }
})


// util.js
/**
 * 文件路径名转格式:例:'./a/b.js' -> 'a_b'
 * @param {*} str String
 * @returns String
 */
export function resetModelName(str) {
    str = str.replace(/.js$/g, '')
    str = str.replace(/\.+\//g, '')
    str = str.replace(/\//g, '_')
    str = str.replace(/\//g, '')
    return str
}

按约定好的规则,后面在model目录内添加的模块,会被自动引入到vuex实例中。或者也可以在此基础之上再进行封装,做成可配置化,提供例如 命名规则、是否查找子目录、自动话开关等等配置项。无论是简单的约定规则,还是复杂强大的配置化规则,都会对前端整个团队的规范有帮助。

Vite

require.context是将匹配到资源静态打包引入,虽然这种方式的确避免了很多重复工作,但是却也在这方面牺牲了一些性能

而如果你用的是vue3.x+Vite,那么可以试试Glob 导入Vite官方关于Glob 导入的文档

Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块:

const modules = import.meta.glob('./dir/*.js')

上面代码会被编译为

// vite 生成的代码
const modules = {
  './dir/foo.js': () => import('./dir/foo.js'),
  './dir/bar.js': () => import('./dir/bar.js')
}

这种方式是懒加载引入的,但是就不能支持复杂的文件目录节构了,需要将你所有的模块放在统一的一个文件目录下,除了这种懒加载的模式,还有一种import.meta.globEager是直接引入所有模块,详细可以去阅读官网文档
需要注意的是这种方式是Vite特有的!