都说前端工程化,但你真的了解require.context()么?(讨论下vue路由懒加载失效的问题)

本文感谢辛苦认真工作的同事和足够多的会使我有足够的的时间出来摸鱼,文章思路比较发散,请勿介意,如果不同见解,欢迎讨论

本文想讨论的几个问题

  1. require.context 的 三个 四个 参数
  2. 前端路由自动化引入(vue)
  3. require.context导致vue 路由懒加载失效

1.require.context

require

我们平常require是在运行时加载模块并且生成一个对象,特点是每次引入一个,需要指定路径,不支持传入变量.

代码试例:

require('./template/' + name + '.ejs');
复制代码

webpack中,webpack为我们提供了require.context这个 api,他和require最大的区别就在于就是可以在一个路径下,获取一个特定的上下文,可以用正则去查找并导入多个模块

让我们来看看什么是 require.context,具体的可以去官网看, webpack 依赖

require.context

和官网雷同的话就复制粘贴稍加解释,主要说说代码里有但是文档里没有的东西

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

  1. 一个要搜索的目录,
  2. 一个标记表示是否还搜索其子目录,
  3. 一个匹配文件的正则表达式。
  4. 第四个参数:mode,处理 import 的时机和类型

语法如下:

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

我们现在只要简单了解一下他的功能与语法,让我们从需求出发来使用一下.

2.前端自动化引入(vue)

这玩意帖子很多,大家在公司项目和自己学习的时候应该都遇到过和写过,一般是为了解决这几个痛点:

  1. 路由,vuex - 每次新增页面要手动维护一个路由或者模块导致的代码臃肿和分模块的维护困难
  2. 全局组件 - 本地的全局组件在 main 批量导入
  3. 其他一些基于文件路径就可以做出区分并与业务逻辑关联不大的引用

在这边不细说路由,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

image.png

我们在官网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
  • eagerrequire.context选项里可以基本等同于sync(个人理解)
  • weaksync的基础上添加一个weak标识,彻底阻止额外的网络请求。只有当该模块已在其他地方被加载过了之后,Promise才被 resolve,否则直接被 reject。
  • async-weakrequire.context选项里可以基本等同于weak(个人理解)

我们在上面遇到懒加载不生效就是因为require.context里默认是使用sync模式进行对引入组件的处理,导致分包被阻断,我们只要加上lazy或者lazy-once便可以解决问题.

lay.png

lazylazy-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)};`;
};

复制代码

总结

  1. require.context其实具有四个属性,在不同场景不同优化方案下会起到不同作用

  2. 使用自动化引入可能可以尝试一些其他方案,不一定要完全依赖于webpack

  3. webpack好难,谁来带带我

  4. 下期专门写一下webpack的有用注释(插旗)

  5. 封面是龙与虎

猜你喜欢

转载自juejin.im/post/7036711224498716680