本文感谢辛苦认真工作的同事和足够多的会使我有足够的的时间出来摸鱼,文章思路比较发散,请勿介意,如果不同见解,欢迎讨论
本文想讨论的几个问题
- require.context 的
三个四个 参数 - 前端路由自动化引入(vue)
- require.context导致vue 路由懒加载失效
1.require.context
require
我们平常require
是在运行时加载模块并且生成一个对象,特点是每次引入一个,需要指定路径,不支持传入变量.
代码试例:
require('./template/' + name + '.ejs');
复制代码
在webpack
中,webpack
为我们提供了require.context
这个 api,他和require
最大的区别就在于就是可以在一个路径下,获取一个特定的上下文,可以用正则去查找并导入多个模块
让我们来看看什么是 require.context,具体的可以去官网看, webpack 依赖
require.context
和官网雷同的话就复制粘贴稍加解释,主要说说代码里有但是文档里没有的东西
可以给这个函数传入三个参数:
- 一个要搜索的目录,
- 一个标记表示是否还搜索其子目录,
- 一个匹配文件的正则表达式。
- 第四个参数:mode,处理 import 的时机和类型
语法如下:
require.context(
directory,
(useSubdirectories = true),
(regExp = /^\.\/.*$/),
(mode = 'sync')
);
复制代码
我们现在只要简单了解一下他的功能与语法,让我们从需求出发来使用一下.
2.前端自动化引入(vue)
这玩意帖子很多,大家在公司项目和自己学习的时候应该都遇到过和写过,一般是为了解决这几个痛点:
- 路由,vuex - 每次新增页面要手动维护一个路由或者模块导致的代码臃肿和分模块的维护困难
- 全局组件 - 本地的全局组件在 main 批量导入
- 其他一些基于文件路径就可以做出区分并与业务逻辑关联不大的引用
在这边不细说路由,vuex 自动化导入的具体实现,我们只是来体验一下

一个简单的按照文件夹区分路由的改造
最开始是这样,每次新建一个路由都需要手工添加多行代码,还要添加 name 和一些其他的参数.
export const carRoutes = [
{
path: '/car1',
component: () => import('@/views/car1/index'),
},
{
path: '/car2',
component: () => import('@/views/car1/index'),
},
{
path: '/car3',
component: () => import('@/views/car1/index'),
},
];
复制代码
经过初步改造,现在只要每次新增的时候新增一行代码(可以直接跳过)
function createCarRoute(name) {
return {
path: `/${name}`,
name,
component: () =>
import(/* webpackChunkName: "[request]" */ `@/views/car/${name}`),
};
}
export const externalCarRoutes = [
createCarRoute('xxx1'),
createCarRoute('xxx2'),
createCarRoute('xxx3'),
];
复制代码
使用require.context
看起来代码也不短,但是可以一劳永逸.
export const carRoutes = require
//根据命名规范 index.vue代表每个文件夹下的主页面,需要路由
.context('@/views/car', true, /\/index\.vue$/)
.keys()
.map((url) => url.replace(/^\.\//, '').replace(/\/index\.vue$/, ''))
.map((pathName) => {
return {
path: `/${pathName}`,
name: pathName,
component: () =>
import(
/* webpackChunkName: "[page]" */ `@/views/car/${pathName}/index.vue`
),
};
});
复制代码
上述的代码,我们主要关注第三段,也就是本文的重点require.context
我们使用这个方法遍历了view/car
下的所有文件和文件夹,将其中的主页面生成 key 并导入.
/TODO/context 代码解读
3.vue 路由懒加载
上面的代码中其实已经实现了基本的基于 import
实现的路由懒加载 在上述 return 的对象中可以发现使用 import
直接返回 page 的路径
仔细看的话可以发现这里有个注释/* webpackChunkName: "[page]" */
,这是 webpack 会识别的一个分割包的注释,我们不用管,或者说,我们等下再管,直接去看重头戏.
{
path: `/${pathName}`,
name: pathName,
component: () =>
import(
/* webpackChunkName: "[page]" */ `@/views/car/${pathName}/index.vue`
),
};
复制代码
上面这块代码,在平常的时候可以生效,但是在require.context
中,会发现这个公认的懒加载方案不生效了! 左改右改,都不行,最后通过看打包后的app.js
进行控制变量对比才发现问题就出在require.context
中 仔细查阅了外面的其他文档和看了内部代码,来引入今天的头号嘉宾webpackMode
4.重头戏 第四个参数 webpackMode !
webpackMode options
我们在官网webpack 4的文档中并没有找到这个参数,在 5代中虽然提到了这个参数,文档中并没有很明确的说明mode的含义. 我们从源码中拉出对应的typeof
/** @typedef {"sync" | "eager" | "weak" | "async-weak" | "lazy" | "lazy-once"} ContextMode Context mode */
复制代码
webpackMode属性定义了 resolve 动态模块时的模式。支持以下六种模式:
- sync 默认属性,不生成额外的
chunk
。所有导入的模块被包含在当前模块内,所以不需要再发额外的网络请求。它仍然返回一个Promise
,但它被自动 resolve。使用sync
模式的动态导入与静态导入的区别在于,整个模块只有当import()
调用之后才执行. - lazy 为动态引入的模块建立动态
chunk
- lazy-once 使用它,会为满足导入条件的所有模块创建单一的异步
chunk
。 - eager 在
require.context
选项里可以基本等同于sync
(个人理解) - weak 在
sync
的基础上添加一个weak
标识,彻底阻止额外的网络请求。只有当该模块已在其他地方被加载过了之后,Promise
才被 resolve,否则直接被 reject。 - async-weak 在
require.context
选项里可以基本等同于weak
(个人理解)
我们在上面遇到懒加载不生效就是因为require.context
里默认是使用sync
模式进行对引入组件的处理,导致分包被阻断,我们只要加上lazy
或者lazy-once
便可以解决问题.
lazy
和lazy-once
的区别便是一个按文件数量生成chunk
,lazy-once
把上下文内符合规则的模块打到一起,只生成一个chunk
webpackMode注释
我们默认大家已经知道和了解webpack的一些魔法注释,但这边还是稍加解释一下webpackMode
指定webpack引入包的类型 在 webpack import
中,默认会使用lazy
作为
route: () => import(/* webpackMode: "eager" */ "./.vue")
复制代码
这里要注意,如果上面配了lazy-once
的话,在下面引入的地方也要加上魔法注释类型为lazy-once
这里感觉牵扯到一个优先级的问题,此处的webpackMode注释
和require.context
中的可选类型是一支,只是在import
中注释会有更多功能
webpackChunkName
我们继续默认大家已经知道和了解webpack的一些魔法注释,但这边还是稍加解释一下webpackChunkName
这个分包命名的注释. 他的使用方式很简单,在引入模块的地方添加一段注释便可以给分出来包重新命名
/* webpackChunkName: "[request]" */
复制代码
里面有两个特殊的变量 [index]
和[request]
, 以下两个变量生效的前提是有多个动态导入的文件
[index]
表示在当前动态导入声明中表示文件的索引。 [request]
表示可以根据动态导入的文件名进行命名.
现有项目和路由迁移
可以基于glob或者其他的工具基于babel ast对现有的路由进行改造,验证是可行的,主要是现有项目路由太多,不适合手改,等我学懂了就去优化
const parse = require('@babel/parser').parse;
const traverse = require('@babel/traverse').default;
const path = require('path');
const glob = require('glob');
function transformRouter(targetPath) {
const realpath = targetPath.replace('@', 'src');
const files = glob.sync(`${realpath}/*`);
let res = '';
for (let dir of files) {
const indexVue = glob.sync(`${dir}/index.vue`);
if (indexVue && indexVue.length > 0) {
const basename = path.basename(dir);
res = `${res}{
path: '/${basename}',
name: '${basename}',
component: r => require.ensure([], () => r(require('${targetPath}/${basename}/index.vue')), '${basename}'),
},`;
}
}
if (res) {
return `[${res}]`;
}
return '[]';
}
module.exports = function(content) {
const ast = parse(content, {
allowImportExportEverywhere: true,
});
let exportName = '';
let targetPath = '';
traverse(ast, {
ExportNamedDeclaration(path) {
const node = path.node;
if (
node.declaration &&
node.declaration.kind === 'const' &&
Array.isArray(node.declaration.declarations) &&
node.declaration.declarations.length > 0
) {
const targetDeclaration = node.declaration.declarations[0];
exportName = targetDeclaration.id.name;
}
},
CallExpression(path) {
const node = path.node;
if (
node.callee &&
node.callee.object &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'require' &&
node.callee.property &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'context' &&
Array.isArray(node.arguments) &&
node.arguments[0]
) {
targetPath = node.arguments[0].value;
}
},
});
if (!targetPath || !exportName) return content;
return `export const ${exportName} = ${transformRouter(targetPath)};`;
};
复制代码
总结
-
require.context
其实具有四个属性,在不同场景不同优化方案下会起到不同作用 -
使用自动化引入可能可以尝试一些其他方案,不一定要完全依赖于webpack
-
webpack好难,谁来带带我
-
下期专门写一下webpack的有用注释(插旗)
-
封面是龙与虎