Webpack resolve解析

虽然在实际开发过程中,我们经常会使用脚手架来初始化一个项目,而脚手架一般都包含了完善的webpack配置,例如create-react-app这个脚手架,其把所有关于构建的内容封装在了react-scripts包中,在实际开发中,我们只需要运行yarn run start/build/test即可,它就可以帮我们搞定代码压缩、分隔,jsxes6代码编译到es5等。

回到webpack,我们都知道一个完整的webpack配置必定是要包含入口(entry)输出(output),可能还需要模块-加载器(loader)来处理不同类型的模块、或是使用插件(plugin)来在构建过程中自定义某些动作。除此之外,还有webpack4中才引入的用于性能和构建优化的optimization,还有用于开发环境的开发服务器(devServer)。还有不那么常用和深入人心的解析(resolve)。本篇将以react-scripts包的webpack配置中关于resolve的使用为基础,介绍如何在实际项目中可能会用到的自定义解析。

他们是怎么写的

首先来看看react-scripts关于resolve的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
resolve: {
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
extensions: paths.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: {
'react-native': 'react-native-web',
},
plugins: [
PnpWebpackPlugin,
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
],
},

  1. modules属性:其指定了webpack在进行模块解析时应该搜索的目录,该属性可通过数组的方式指定一系列的路径,默认值是: ["node_modules"],也就是说,在不指定该属性的情况下,如果我们引入import xx from xx,则webpack会默认在根目录的node_modules目录下查找该模块。上面的配置中指定了额外两种模块解析路径,其分别是path.appNodeModulesmodules.additionalModulePaths,其中path.appNodeModules最终指向的是path.resolve(fs.realpathSync(process.cwd()), 'node_modules')也就是说,在默认情况下,该路径是node_modules决定路径modules.additionalModulePaths指向一个自定义路径,并通过getAdditionalModulePaths(config)方法生成该路径,如果config等于{},则返回process.env.NODE_PATH(经过一些列的处理),否则的话如果config.baseUrl存在且等于modules.additionalModulePaths或者appSrc则返回,否则抛出错误。也就是说,我们可以通过jsconfig.json来指定baseUrl属性,并且该属性只能是node_modules目录或src目录。

  2. extensions属性:自动解析的确定的扩展,默认值是['.js', '.json'],也就是说,在默认情况下,我们import ClsA from './clsa',可以解析到clsa.js或是cls.json。上面的配置重写了extensions,定义了更多的扩展,并根据当前是否是typescript项目而是用对应的拓展。paths.moduleFileExtensions定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const moduleFileExtensions = [
    'web.mjs',
    'mjs',
    'web.js',
    'js',
    'web.ts',
    'ts',
    'web.tsx',
    'tsx',
    'json',
    'web.jsx',
    'jsx',
    ];

如何判别当前项目是typescript项目呢?
useTypeScript是这样定义的:const useTypeScript = fs.existsSync(paths.appTsConfig);,很明显,通过判断是否存在paths.appTsConfig指向的文件也就是tsconfig.json,如果存在该文件,则表示该项目是中可以引用那些以ts为后缀的文件而不需要指定扩展名。

  1. alias属性:创建模块的别名,确保在引入某些模块时可以变得简单。在上面中alias的配置是:
    1
    2
    3
    alias: {
    'react-native': 'react-native-web',
    },

也就是在我们使用名为react-native的模块时,其默认指向的是react-native-web模块,例如:

1
import { View } from 'react-native';

此时项目中根本没安装react-native,仅安装了react-native-web,也就是说react-native成了react-native-web的别名。那么问题来了:为什么我们不直接写import { View } from 'react-native-web'呢?

其实这涉及到React-Native强调的一次编写,处处使用,这里的使用并不仅仅是iOSAndroid代码共用,而是React-NativeWeb之间的代码共享。react-native-web就是这样一个库,它把react-native实现的组件实现成为Web组件,并且表现和react-native组件一致。这样,如果我们拿到的是一份react-native的代码,添加别名过后,就无需把所有的react-native全都改成react-native-web,这样就保证了两端代码的统一。

扯远了,继续。

  1. plugins属性:如果在配置解析的过程中需要插件的话,就可以在这里指定。上面的代码使用了pnp-webpack-pluginreact-dev-utils/ModuleScopePluginpnp-webpack-plugin是为了解决require()时过多的I/O操作带来的性能消耗,pnp思想来自Yarn团队,目的是为了解决安装和引用依赖效率过低问题。其建立了一张映射表,这张表记录了依赖版本关联和依赖与依赖之间的关联以及依赖存放的位置。有了这张表,就可以跳过繁琐的查找过程直接确定依赖在文件中的位置,从而提高性能。详情见stackoverflowModuleScopePlugin插件的官方解释是:该插件可以确保来自源程序目录(也就是/src)的相对导入不会使用到外部依赖。new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson])接收两个参数,分别指定了源程序目录/srcallowedFiles指向了package.json文件。

更多的配置

  1. mainFields属性:该属性是为了指定在不同环境中,默认使用哪个webpack的字段作为导入的入口。例如某个模块的package.json文件中执行了以下入口:
    1
    2
    3
    4
    5
    {
    module: 'index.js',
    main: 'build/index.node.js',
    browser: 'build/index.js',
    }

那么在Node环境下,默认会使用build/index.node.js导入,而在浏览器环境中,默认使用build/index.js导入。

  1. mainFiles属性:解析目录是默认使用的文件名。这个在实际开发中使用得比较多,例如我们开发的某个页面,文件路径为/src/views/Home/index.js,那么在实际使用这个页面的时候直接使用import Home from './src/views/Home'即可,因为mainFiles默认的配置是:['index'],这个属性不建议自定义。

resolve还有很多属性,可以让我们充分自定义整个解析过程,但从react-scripts的实践上来看,其也是针对某些属性进行了定制,并没有一味的自定义。在对某个属性不是很熟悉并且没有过实践,建议不要盲目的修改。并且resolve的所有属性都提供了适应绝普通场景的默认值。最后,resolve使用愉快。

完。

分享到 评论