Create React App 源码揭秘

2022-02-14 16:52:18 浏览数 (1)

目录

  • 背景
  • monorepo管理
    • monorepo优势
    • monorepo劣势
  • Lerna
    • 全局安装Lerna
    • 初始化项目
    • 创建Package
    • 开启Workspace
    • LernaScript
  • CreateReactApp架构
  • packages/create-react-app
    • 准备工作
    • 创建package.json
    • 安装依赖项
    • 拷贝模板
    • 查看效果
  • packages/cra-template
  • packages/cra-template--typescript
  • packages/react-scripts
    • react-scripts build
    • react-scripts start
    • react-scripts小结
  • packages/react-dev-utils
    • PnpWebpackPlugin
    • ModuleScopePlugin
    • InterpolateHtmlPlugin
    • WatchMissingNodeModulesPlugin
  • 总结

背景

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

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

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

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

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

monorepo管理

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

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

babel的packages目录下存放了多个包。

monorepo优势

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

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

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

目前大多数开源库都使用Monorepo进行管理,如react、vue-next、create-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
}

创建configwebpackDevServer.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

  • PnpWebpackPlugin。提供一种更加高效的模块查找机制,试图取代node_modules
  • ModuleScopePlugin。阻止用户从src/(或node_modules/)外部导入文件。
  • InterpolateHtmlPlugin。使得<link rel="icon" href="%PUBLIC_URL%/favicon.ico">中可以使用变量%PUBLIC_URL%
  • WatchMissingNodeModulesPlugin。使得安装了新的依赖不再需要重新启动项目也能正常运行。
代码语言: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

0 人点赞