前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Create React App 源码揭秘

Create React App 源码揭秘

作者头像
Careteen
发布2022-02-14 16:52:18
3.6K0
发布2022-02-14 16:52:18
举报
文章被收录于专栏:源码揭秘

目录

背景

文章首发于@careteen/create-react-app,转载请注明来源即可。

Create React App是一个官方支持的创建React单页应用程序的脚手架。它提供了一个零配置的现代化配置设置。

平时工作中一部分项目使用的React,使用之余也需要了解其脚手架实现原理。

之前做的模板项目脚手架@careteen/cli,实现方式比较原始。后续准备通过lerna进行重构。

下面先做一些前备知识了解。

monorepo管理

如果对monorepo和lerna已经比较了解,可以直接移步CreateReactApp架构

Monorepo是管理项目代码的一个方式,指在一个项目仓库(repo)中管理多个模块/包(package)。不同于常见的每个模块都需要建一个repo

babelpackages目录下存放了多个包。

monorepo优势

Monorepo最主要的好处是统一的工作流代码共享

比如我在看babel-cli的源码时,其中引用了其他库,如果不使用Monorepo管理方式,而是对@babel/core新建一个仓库,则需要打开另外一个仓库。如果直接在当前仓库中查看,甚至修改进行本地调试,那阅读别人代码会更加得心应手。

代码语言:javascript
复制
import { buildExternalHelpers } from "@babel/core";

目前大多数开源库都使用Monorepo进行管理,如reactvue-nextcreate-react-app

monorepo劣势

  • 体积庞大。babel仓库下存放了所有相关代码,clone到本地也需要耗费不少时间。
  • 不适合用于公司项目。各个业务线仓库代码基本都是独立的,如果堆放到一起,理解和维护成本将会相当大。

Lerna

如果对monorepo和lerna已经比较了解,可以直接移步CreateReactApp架构

Lernababel团队对Monorepo的最佳实践。是一个管理多个npm模块的工具,有优化维护多个包的工作流,解决多个包互相依赖,且发布需要手动维护多个包的问题。

前往lerna查看官方文档,下面做一个简易入门。

全局安装Lerna

代码语言:javascript
复制
$ npm i -g lerna

初始化项目

代码语言:javascript
复制
$ mkdir lerna-example && cd $_
$ lerna init

生成项目结构

代码语言:javascript
复制
|-- lerna.json
|-- package.json
`-- packages # 暂时为空文件夹

packages.json文件中指定packages工作目录为packages/*下所有目录

代码语言:javascript
复制
{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}

创建Package

代码语言:javascript
复制
# 一路回车即可
$ lerna create create-react-app
$ lerna create react-scripts
$ lerna create cra-template

会在packages/目录下生成三个子项目

开启Workspace

默认是npm,每个子package都有自己的node_modules

新增如下配置,开启workspace。目的是让顶层统一管理node_modules,子package不管理。

代码语言:javascript
复制
// package.json
{
  "private": true,
  "workspaces": [
    "packages/*"
  ],
}
代码语言:javascript
复制
// lerna.json
{
  "useWorkspaces": true,
  "npmClient": "yarn"
}

Lerna Script

前往Lerna查看各个command的详细使用

  • lerna add
  • lerna bootstrap
  • lerna list
  • lerna link
  • lerna publish
lerna add
代码语言:javascript
复制
# 语法
$ lerna add <package>[@version] [--dev] [--exact] [--peer]
代码语言:javascript
复制
# 示例
# 为所有子`package`都安装`chalk`
$ lerna add chalk
# 为`create-react-app`安装`commander`
$ lerna add commander --scope=create-react-app
# 如果安装失败,请检查拼写是否错误或者查看子包是否有命名空间
$ lerna list
# 由于我的包做了命名空间,所以需要加上前缀
$ lerna add commander --scope=@careteen/create-react-app

如果想要在根目录为所有子包添加统一依赖,并只在根目录下package.josn,可以借助yarn

代码语言:javascript
复制
yarn add chalk --ignore-workspace-root-check

还能在根目录为某个子package安装依赖

代码语言:javascript
复制
# 子包有命名空间需要加上
yarn workspace create-react-app add commander
lerna bootstrap

默认是npm i,指定使用yarn后,就等价于yarn install

lerna list

列出所有的包

代码语言:javascript
复制
$ lerna list

打印结果

代码语言:javascript
复制
info cli using local version of lerna
lerna notice cli v3.22.1
@careteen/cra-template
@careteen/create-react-app
@careteen/react-scripts
lerna success found 3 packages
lerna link

建立软链,等价于npm link

lerna publish
代码语言:javascript
复制
$ lerna publish              # 发布自上次发布以来已经更改的包
$ lerna publish from-git     # 显式发布在当前提交中标记的包
$ lerna publish from-package # 显式地发布注册表中没有最新版本的包
第一次发布报错
  • 原因

第一次leran publish发布时会报错lerna ERR! E402 You must sign up for private packages,原因可查看lerna #1821

  • 解决方案

以下操作需要保证将本地修改都git push,并且将npm registry设置为 https://registry.npmjs.org/且已经登录后。

  1. 由于npm限制,需要先在package.json中做如下设置
代码语言:javascript
复制
"publishConfig": {
  "access": "public"
},
  1. 然后前往各个子包先通过npm publish发布一次
代码语言:javascript
复制
$ cd packages/create-react-app && npm publish --access=public
  1. 修改代码后下一次发布再使用lerna publish,可得到如下日志
代码语言:javascript
复制
$ lerna publish
  Patch (0.0.1) # 选择此项并回车
  Minor (0.1.0) 
  Major (1.0.0) 
  Prepatch (0.0.1-alpha.0) 
  Preminor (0.1.0-alpha.0) 
  Premajor (1.0.0-alpha.0) 
  Custom Prerelease 
  Custom Version

? Select a new version (currently 0.0.0) Patch (0.0.1)

Changes:
 - @careteen/cra-template: 0.0.1 => 0.0.1
 - @careteen/create-react-app: 0.0.1 => 0.0.1
 - @careteen/react-scripts: 0.0.1 => 0.0.1  
? Are you sure you want to publish these packages? (ynH) # 输入y并回车

Successfully published: # 发布成功
 - @careteen/cra-template@0.0.2
 - @careteen/create-react-app@0.0.2
 - @careteen/react-scripts@0.0.2
lerna success published 3 packages

如果此过程又失败并报错lerna ERR! fatal: tag 'v0.0.1' already exists,对应issues可查看lerna #1894。需要先将本地和远程tag删除,再发布。

代码语言:javascript
复制
# 删除本地tag
git tag -d v0.0.1
# 删除远程tag
git push origin :refs/tags/v0.0.1
# 重新发布
lerna publish

CreateReactApp架构

packages/create-react-app

准备工作

在项目根目录package.json文件新增如下配置

代码语言:javascript
复制
"scripts": {
  "create": "node ./packages/create-react-app/index.js"
}

然后在packages/create-react-app/package.json新增如下配置

代码语言:javascript
复制
"main": "./index.js",
"bin": {
  "careteen-cra": "./index.js"
},

新增packages/create-react-app/index.js文件

代码语言:javascript
复制
#!/user/bin/env node
const { init } = require('./createReactApp')
init()

新增packages/create-react-app/createReactApp.js文件

代码语言:javascript
复制
const chalk = require('chalk')
const { Command } = require('commander')
const packageJson = require('./package.json')

const init = async () => {
  let appName;
  new Command(packageJson.name)
    .version(packageJson.version)
    .arguments('<project-directory>')
    .usage(`${chalk.green('<project-directory>')} [options]`)
    .action(projectName => {
      appName = projectName
    })
    .parse(process.argv)
  console.log(appName, process.argv)
}
module.exports = {
  init,
}

在项目根目录运行

代码语言:javascript
复制
# 查看包版本
npm run create -- --version
# 打印出`myProject`
npm run create -- myProject

会打印myProject,`[ '/Users/apple/.nvm/versions/node/v14.8.0/bin/node', '/Users/apple/Desktop/create-react-app/packages/create-react-app/index.js', 'myProject' ]`

创建package.json

先添加依赖

代码语言:javascript
复制
# cross-spawn 跨平台开启子进程
# fs-extra fs增强版
yarn add cross-spawn fs-extra --ignore-workspace-root-check

在当前工作环境创建myProject目录,然后创建package.json文件写入部分配置

代码语言:javascript
复制
const fse = require('fs-extra')
const init = async () => {
  // ...
  await createApp(appName)
}
const createApp = async (appName) => {
  const root = path.resolve(appName)
  fse.ensureDirSync(appName)
  console.log(`Creating a new React app in ${chalk.green(root)}.`)
  const packageJson = {
    name: appName,
    version: '0.1.0',
    private: true,
  }
  fse.writeFileSync(
    path.join(root, 'package.json'),
    JSON.stringify(packageJson, null, 2)
  )
  const originalDirectory = process.cwd()
  
  console.log('originalDirectory: ', originalDirectory)
  console.log('root: ', root)
}

安装依赖项

然后改变工作目录为新创建的myProject目录,确保后续为此目录安装依赖react, react-dom, react-scripts, cra-template

代码语言:javascript
复制
const createApp = async (appName) => {
  // ...
  process.chdir(root)
  await run(root, appName, originalDirectory)
}
const run = async (root, appName, originalDirectory) => {
  const scriptName = 'react-scripts'
  const templateName = 'cra-template'
  const allDependencies = ['react', 'react-dom', scriptName, templateName]
  console.log(
    `Installing ${chalk.cyan('react')}, ${chalk.cyan(
      'react-dom'
    )}, and ${chalk.cyan(scriptName)}${
      ` with ${chalk.cyan(templateName)}`
    }...`
  )
}

此时我们还没有编写react-scripts, cra-template这两个包,先使用现有的。

后面实现后可改为@careteen/react-scripts, @careteen/cra-template

代码语言:javascript
复制
lerna add react-scripts cra-template --scope=@careteen/create-react-app

借助cross-spawn开启子进程安装依赖

代码语言:javascript
复制
const run = async (root, appName, originalDirectory) => {
  // ...
  await install(root, allDependencies)
}
const install = async (root, allDependencies) => {
  return new Promise((resolve) => {
    const command = 'yarnpkg'
    const args = ['add', '--exact', ...allDependencies, '--cwd', root]
    const child = spawn(command, args, {
      stdio: 'inherit',
    })
    child.on('close', resolve)
  })
}

拷贝模板

核心部分在于运行react-scripts/scripts/init.js做模板拷贝工作。

代码语言:javascript
复制
const run = async (root, appName, originalDirectory) => {
  // ...
  await install(root, allDependencies)
  const data = [root, appName, true, originalDirectory, templateName]
  const source = `
  var init = require('react-scripts/scripts/init.js');
  init.apply(null, JSON.parse(process.argv[1]));
  `
  await executeNodeScript(
    {
      cwd: process.cwd(),
    },
    data,
    source,
  )
  console.log('Done.')
  process.exit(0)
}
const executeNodeScript = async ({ cwd }, data, source) => {
  return new Promise((resolve) => {
    const child = spawn(
      process.execPath,
      ['-e', source, '--', JSON.stringify(data)],
      {
        cwd,
        stdio: 'inherit',
      }
    )
    child.on('close', resolve)
  })
}

其中spawn(process.execPath, args, { cwd })类似于我们直接在terminal中直接使用node -e 'console.log(1 + 1)',可以直接运行js代码。

查看效果

运行下面脚本

代码语言:javascript
复制
npm run create -- myProject

可以在当前项目根目录看到myProject的目录结构。

此时已经实现了create-react-app`package的核心功能。下面将进一步剖析cra-tempalte, react-scripts`。

packages/cra-tempalte

cra-tempalte可以从cra-tempalte拷贝,启动一个简易React单页应用。

React原理感兴趣的可前往由浅入深React的Fiber架构查看。

packages/cra-tempalte--typescript

同上,不是本文讨论重点。

packages/react-scripts

安装依赖

代码语言:javascript
复制
# `lerna`给子包装多个依赖时报警告`lerna WARN No packages found where webpack can be added.`
lerna add webpack webpack-dev-server babel-loader babel-preset-react-app html-webpack-plugin open --scope=@careteen/react-scripts
# 故使用`yarn`安装
yarn workspace @careteen/react-scripts add webpack webpack-dev-server babel-loader babel-preset-react-app html-webpack-plugin open

package.json配置

代码语言:javascript
复制
"bin": {
  "careteen-react-scripts": "./bin/react-scripts.js"
},
"scripts": {
  "start": "node ./bin/react-scripts.js start",
  "build": "node ./bin/react-scripts.js build"
},

创建bin/react-scripts.js文件

代码语言:javascript
复制
#!/usr/bin/env node
const spawn = require('cross-spawn')
const args = process.argv.slice(2)
const script = args[0]
spawn.sync(
  process.execPath,
  [require.resolve('../scripts/' + script)],
  { stdio: 'inherit' }
)

react-scripts build

webpack原理感兴趣的可前往@careteen/webpack查看简易实现。

创建scripts/build.js文件,主要负责两件事

  • 拷贝模板项目的public目录下的所有静态资源到build目录下
  • 配置为production环境,使用webpack(config).run()编译打包
代码语言:javascript
复制
process.env.NODE_ENV = 'production'
const chalk = require('chalk')
const fs = require('fs-extra')
const webpack = require('webpack')
const configFactory = require('../config/webpack.config')
const paths = require('../config/paths')
const config = configFactory('production')

fs.emptyDirSync(paths.appBuild)
copyPublicFolder()
build()

function build() {
  const compiler = webpack(config)
  compiler.run((err, stats) => {
    console.log(err)
    console.log(chalk.green('Compiled successfully.\n'))
  })
}
function copyPublicFolder() {
  fs.copySync(paths.appPublic, paths.appBuild, {
    filter: file => file !== paths.appHtml,
  })
}

配置config/webpack.config.js文件

代码语言:javascript
复制
const paths = require('./paths')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = function (webpackEnv) {
  const isEnvDevelopment = webpackEnv === 'development'
  const isEnvProduction = webpackEnv === 'production'
  return {
    mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
    output: {
      path: paths.appBuild
    },
    module: {
      rules: [{
        test: /\.(js|jsx|ts|tsx)$/,
        include: paths.appSrc,
        loader: require.resolve('babel-loader'),
        options: {
          presets: [
            [
              require.resolve('babel-preset-react-app')
            ]
          ]
        }
      }, ]
    },
    plugins: [
      new HtmlWebpackPlugin({
        inject: true,
        template: paths.appHtml
      })
    ]
  }
}

配置config/paths.js文件

代码语言:javascript
复制
const path = require('path')
const appDirectory = process.cwd()
const resolveApp = relativePath => path.resolve(appDirectory, relativePath)
module.exports = {
  appHtml: resolveApp('public/index.html'),
  appIndexJs: resolveApp('src/index.js'),
  appBuild: resolveApp('build'),
  appPublic: resolveApp('public')
}

npm run build后可查看build目录下会生成编译打包后的所有文件

react-scripts start

创建scripts/start.js文件,借助webpack功能启服务

代码语言:javascript
复制
process.env.NODE_ENV = 'development'
const configFactory = require('../config/webpack.config')
const createDevServerConfig = require('../config/webpackDevServer.config')
const WebpackDevServer = require('webpack-dev-server')
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000
const HOST = process.env.HOST || '0.0.0.0'
const config = configFactory('development')
const webpack = require('webpack')
const chalk = require('chalk')
const compiler = createCompiler({
  config,
  webpack
})
const serverConfig = createDevServerConfig()
const devServer = new WebpackDevServer(compiler, serverConfig)
devServer.listen(DEFAULT_PORT, HOST, err => {
  if (err) {
    return console.log(err)
  }
  console.log(chalk.cyan('Starting the development server...\n'))
})

function createCompiler({
  config,
  webpack
}) {
  let compiler = webpack(config)
  return compiler
}

创建config\webpackDevServer.config.js文件提供本地服务设置

webpack热更新原理感兴趣的可前往@careteen/webpack-hmr查看简易实现。

代码语言:javascript
复制
module.exports = function () {
  return {
    hot: true
  }
}

npm run start后可在浏览器 http://localhost:8080/ 打开查看效果

react-scripts小结

上面两节实现没有源码考虑的那么完善。后面将针对源码中使用到的一些较为巧妙的第三方库和webpack-plugin做讲解。

packages/react-dev-utils

此子package下存放了许多webpack-plugin辅助于react-scripts/config/webpack.config.js文件。在文件中搜索plugins字段查看。

此文先列举一些我觉得好用的plugins

代码语言:javascript
复制
return {
  // ...
  resolve: {
    plugins: [
      // 增加了对即插即用(Plug'n'Play)安装的支持,提高了安装速度,并增加了对遗忘依赖项等的保护。
      PnpWebpackPlugin,
      // 阻止用户从src/(或node_modules/)外部导入文件。
      // 这经常会引起混乱,因为我们只使用babel处理src/中的文件。
      // 为了解决这个问题,我们阻止你从src/导入文件——如果你愿意,
      // 请将这些文件链接到node_modules/中,然后让模块解析开始。
      // 确保源文件已经编译,因为它们不会以任何方式被处理。
      new ModuleScopePlugin(paths.appSrc, [
        paths.appPackageJson,
        reactRefreshOverlayEntry,
      ]),
    ],
  },
  plugins: [
    // ...
    // 使一些环境变量在index.html中可用。
    // public URL在index中以%PUBLIC_URL%的形式存在。html,例如:
    // <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
    // 除非你指定"homepage"否则它将是一个空字符串
    // 在包中。在这种情况下,它将是该URL的路径名。
    new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
    // 如果你需要一个缺失的模块,然后用' npm install '来安装它,你仍然需要重启开发服务器,webpack才能发现它。这个插件使发现自动,所以你不必重新启动。
    // 参见https://github.com/facebook/create-react-app/issues/186
    isEnvDevelopment &&
        new WatchMissingNodeModulesPlugin(paths.appNodeModules),
  ]

}

PnpWebpackPlugin

增加了对即插即用(Plug'n'Play)安装的支持,提高了安装速度,并增加了对遗忘依赖项等的保护。试图取代node_modules

先来了解下使用node_modules模式的机制

  1. 将依赖包的版本区间解析为某个具体的版本号
  2. 下载对应版本依赖的tar 报到本地离线镜像
  3. 将依赖从离线镜像解压到本地缓存
  4. 将依赖从缓存拷贝到当前目录的node_modules目录

PnP工作原理是作为上述第四步骤的替代方案

PnP使用

示例存放在plugins-example/PnpWebpackPlugin

create-react-app已经集成了对PnP的支持。只需在创建项目时添加--use-pnp参数。

代码语言:javascript
复制
create-react-app myProject --use-pnp

在已有项目中开启可使用yarn提供的--pnp

代码语言:javascript
复制
yarn --pnp
yarn add uuid

与此同时会自动在package.json中配置开启pnp。而且不会生成node_modules目录,取而代替生成.pnp.js文件。

代码语言:javascript
复制
{
  "installConfig": {
    "pnp": true
  }
}

由于在开启了 PnP 的项目中不再有 node_modules 目录,所有的依赖引用都必须由 .pnp.js 中的 resolver 处理 因此不论是执行 script 还是用 node 直接执行一个 JS 文件,都必须经由 Yarn 处理

代码语言:javascript
复制
{
  // 还需配置使用脚本
  "scripts": {
    "build": "node uuid.js"
  }
}

运行脚本查看效果

代码语言:javascript
复制
yarn run build
# 或者使用node
yarn node uuid.js

ModuleScopePlugin

阻止用户从src/(或node_modules/)外部导入文件。 这经常会引起混乱,因为我们只使用babel处理src/中的文件。 为了解决这个问题,我们阻止你从src/导入文件——如果你愿意, 请将这些文件链接到node_modules/中,然后让模块解析开始。 确保源文件已经编译,因为它们不会以任何方式被处理。

通过create-react-app生成的项目内部引用不了除src外的目录,不然会报错which falls outside of the project src/ directory. Relative imports outside of src/ are not supported.

通常解决方案是借助react-app-rewired, customize-cra解决。

那接下来看看是如何实现这个功能。

示例存放在plugins-example/ModuleScopePlugin

实现步骤主要是

  • 着手于resolver.hooks.file解析器读取文件request时。
  • 解析的文件路径如果包含node_modules则放行。
  • 解析的文件路径如果包含使用此插件的传参appSrc则放行。
  • 解析的文件路径和srcpath.relative,结果如果是以../开头,则认为在src路径之外,会抛错。
代码语言:javascript
复制
const chalk = require('chalk');
const path = require('path');
const os = require('os');

class ModuleScopePlugin {
  constructor(appSrc, allowedFiles = []) {
    this.appSrcs = Array.isArray(appSrc) ? appSrc : [appSrc];
    this.allowedFiles = new Set(allowedFiles);
  }

  apply(resolver) {
    const { appSrcs } = this;
    resolver.hooks.file.tapAsync(
      'ModuleScopePlugin',
      (request, contextResolver, callback) => {
        // Unknown issuer, probably webpack internals
        if (!request.context.issuer) {
          return callback();
        }
        if (
          // If this resolves to a node_module, we don't care what happens next
          request.descriptionFileRoot.indexOf('/node_modules/') !== -1 ||
          request.descriptionFileRoot.indexOf('\\node_modules\\') !== -1 ||
          // Make sure this request was manual
          !request.__innerRequest_request
        ) {
          return callback();
        }
        // Resolve the issuer from our appSrc and make sure it's one of our files
        // Maybe an indexOf === 0 would be better?
        if (
          appSrcs.every(appSrc => {
            const relative = path.relative(appSrc, request.context.issuer);
            // If it's not in one of our app src or a subdirectory, not our request!
            return relative.startsWith('../') || relative.startsWith('..\\');
          })
        ) {
          return callback();
        }
        const requestFullPath = path.resolve(
          path.dirname(request.context.issuer),
          request.__innerRequest_request
        );
        if (this.allowedFiles.has(requestFullPath)) {
          return callback();
        }
        // Find path from src to the requested file
        // Error if in a parent directory of all given appSrcs
        if (
          appSrcs.every(appSrc => {
            const requestRelative = path.relative(appSrc, requestFullPath);
            return (
              requestRelative.startsWith('../') ||
              requestRelative.startsWith('..\\')
            );
          })
        ) {
          const scopeError = new Error(
            `You attempted to import ${chalk.cyan(
              request.__innerRequest_request
            )} which falls outside of the project ${chalk.cyan(
              'src/'
            )} directory. ` +
              `Relative imports outside of ${chalk.cyan(
                'src/'
              )} are not supported.` +
              os.EOL +
              `You can either move it inside ${chalk.cyan(
                'src/'
              )}, or add a symlink to it from project's ${chalk.cyan(
                'node_modules/'
              )}.`
          );
          Object.defineProperty(scopeError, '__module_scope_plugin', {
            value: true,
            writable: false,
            enumerable: false,
          });
          callback(scopeError, request);
        } else {
          callback();
        }
      }
    );
  }
}

InterpolateHtmlPlugin

使一些环境变量在index.html中可用。 public URL在index中以%PUBLIC_URL%的形式存在。html,例如: <link rel="icon" href="%PUBLIC_URL%/favicon.ico"> 除非你指定"homepage"否则它将是一个空字符串 在包中。在这种情况下,它将是该URL的路径名。示例存放在plugins-example/InterpolateHtmlPlugin

实现思路主要是对html-webpack-plugin/afterTemplateExecution模板执行后生成的html文件进行正则替换。

代码语言:javascript
复制
const escapeStringRegexp = require('escape-string-regexp');

class InterpolateHtmlPlugin {
  constructor(htmlWebpackPlugin, replacements) {
    this.htmlWebpackPlugin = htmlWebpackPlugin;
    this.replacements = replacements;
  }

  apply(compiler) {
    compiler.hooks.compilation.tap('InterpolateHtmlPlugin', compilation => {
      this.htmlWebpackPlugin
        .getHooks(compilation)
        .afterTemplateExecution.tap('InterpolateHtmlPlugin', data => {
          // Run HTML through a series of user-specified string replacements.
          Object.keys(this.replacements).forEach(key => {
            const value = this.replacements[key];
            data.html = data.html.replace(
              new RegExp('%' + escapeStringRegexp(key) + '%', 'g'),
              value
            );
          });
        });
    });
  }
}

WatchMissingNodeModulesPlugin

如果你需要一个缺失的模块,然后用' npm install '来安装它,你仍然需要重启开发服务器,webpack才能发现它。这个插件使发现自动,所以你不必重新启动。 参见https://github.com/facebook/c...示例存放在plugins-example/WatchMissingNodeModulesPlugin

实现思路是在生成资源到 output 目录之前emit钩子中借助compilationmissingDependenciescontextDependencies.add两个字段对丢失的依赖重新安装。

代码语言:javascript
复制
class WatchMissingNodeModulesPlugin {
  constructor(nodeModulesPath) {
    this.nodeModulesPath = nodeModulesPath;
  }

  apply(compiler) {
    compiler.hooks.emit.tap('WatchMissingNodeModulesPlugin', compilation => {
      var missingDeps = Array.from(compilation.missingDependencies);
      var nodeModulesPath = this.nodeModulesPath;

      // If any missing files are expected to appear in node_modules...
      if (missingDeps.some(file => file.includes(nodeModulesPath))) {
        // ...tell webpack to watch node_modules recursively until they appear.
        compilation.contextDependencies.add(nodeModulesPath);
      }
    });
  }
}

总结

使用多个仓库管理的优点

  • 各模块管理自由度较高,可自行选择构建工具,依赖管理,单元测试等配套设施
  • 各模块仓库体积一般不会太大

使用多个仓库管理的缺点

  • 仓库分散不好找,当很多时,更加困难,分支管理混乱
  • 版本更新繁琐,如果公共模块版本变化,需要对所有模块进行依赖的更新
  • CHANGELOG梳理异常折腾,无法很好的自动关联各个模块的变动联系,基本靠口口相传

使用monorepo管理的缺点

  • 统一构建工具,对构建工具提出了更高要求,要能构建各种相关模块
  • 仓库体积会变大

使用monorepo管理的优点

  • 一个仓库维护多个模块,不用到处找仓库
  • 方便版本管理和依赖管理,模块之间的引用、调试都非常方便,配合相应工具,可以一个命令搞定
  • 方便统一生成CHANGELOG,配合提交规范,可以在发布时自动生成CHANGELOG,借助Leran-changelog
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-01-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
  • 背景
  • monorepo管理
    • monorepo优势
      • monorepo劣势
      • Lerna
        • 全局安装Lerna
          • 初始化项目
            • 创建Package
              • 开启Workspace
                • Lerna Script
                  • lerna add
                  • lerna bootstrap
                  • lerna list
                  • lerna link
                  • lerna publish
              • CreateReactApp架构
              • packages/create-react-app
                • 准备工作
                  • 创建package.json
                    • 安装依赖项
                      • 拷贝模板
                        • 查看效果
                        • packages/cra-tempalte
                        • packages/cra-tempalte--typescript
                        • packages/react-scripts
                          • react-scripts build
                            • react-scripts start
                              • react-scripts小结
                              • packages/react-dev-utils
                                • PnpWebpackPlugin
                                  • PnP使用
                                • ModuleScopePlugin
                                  • InterpolateHtmlPlugin
                                    • WatchMissingNodeModulesPlugin
                                    • 总结
                                    相关产品与服务
                                    云服务器
                                    云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                                    领券
                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档