博客 / 詳情

返回

Create React App 源碼揭秘

cover

目錄

  • 背景
  • 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目錄下存放了多個包。

babel-packages

monorepo優勢

Monorepo最主要的好處是統一的工作流代碼共享

比如我在看babel-cli的源碼時,其中引用了其他庫,如果不使用Monorepo管理方式,而是對@babel/core新建一個倉庫,則需要打開另外一個倉庫。如果直接在當前倉庫中查看,甚至修改進行本地調試,那閲讀別人代碼會更加得心應手。

import { buildExternalHelpers } from "@babel/core";

目前大多數開源庫都使用Monorepo進行管理,如react、vue-next、create-react-app。

monorepo劣勢

  • 體積龐大。babel倉庫下存放了所有相關代碼,clone到本地也需要耗費不少時間。
  • 不適合用於公司項目。各個業務線倉庫代碼基本都是獨立的,如果堆放到一起,理解和維護成本將會相當大。

Lerna

如果對monorepo和lerna已經比較瞭解,可以直接移步CreateReactApp架構

Lernababel團隊對Monorepo的最佳實踐。是一個管理多個npm模塊的工具,有優化維護多個包的工作流,解決多個包互相依賴,且發佈需要手動維護多個包的問題。

前往lerna查看官方文檔,下面做一個簡易入門。

全局安裝Lerna

$ npm i -g lerna

初始化項目

$ mkdir lerna-example && cd $_
$ lerna init

生成項目結構

|-- lerna.json
|-- package.json
`-- packages # 暫時為空文件夾

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

{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}

創建Package

# 一路回車即可
$ lerna create create-react-app
$ lerna create react-scripts
$ lerna create cra-template

會在packages/目錄下生成三個子項目

lerna-create-result

開啓Workspace

默認是npm,每個子package都有自己的node_modules

新增如下配置,開啓workspace。目的是讓頂層統一管理node_modules,子package不管理。

// package.json
{
  "private": true,
  "workspaces": [
    "packages/*"
  ],
}
// lerna.json
{
  "useWorkspaces": true,
  "npmClient": "yarn"
}

Lerna Script

前往Lerna查看各個command的詳細使用
  • lerna add
  • lerna bootstrap
  • lerna list
  • lerna link
  • lerna publish

lerna add

# 語法
$ lerna add <package>[@version] [--dev] [--exact] [--peer]
# 示例
# 為所有子`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

yarn add chalk --ignore-workspace-root-check

還能在根目錄為某個子package安裝依賴

# 子包有命名空間需要加上
yarn workspace create-react-app add commander

lerna bootstrap

默認是npm i,指定使用yarn後,就等價於yarn install

lerna list

列出所有的包

$ lerna list

打印結果

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

$ 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中做如下設置
"publishConfig": {
  "access": "public"
},
  1. 然後前往各個子包先通過npm publish發佈一次
$ cd packages/create-react-app && npm publish --access=public
  1. 修改代碼後下一次發佈再使用lerna publish,可得到如下日誌
$ 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刪除,再發布。

# 刪除本地tag
git tag -d v0.0.1
# 刪除遠程tag
git push origin :refs/tags/v0.0.1
# 重新發布
lerna publish

CreateReactApp架構

structure

packages/create-react-app

準備工作

在項目根目錄package.json文件新增如下配置

"scripts": {
  "create": "node ./packages/create-react-app/index.js"
}

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

"main": "./index.js",
"bin": {
  "careteen-cra": "./index.js"
},

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

#!/user/bin/env node
const { init } = require('./createReactApp')
init()

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

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,
}

在項目根目錄運行

# 查看包版本
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

先添加依賴

# cross-spawn 跨平台開啓子進程
# fs-extra fs增強版
yarn add cross-spawn fs-extra --ignore-workspace-root-check

在當前工作環境創建myProject目錄,然後創建package.json文件寫入部分配置

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

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
lerna add react-scripts cra-template --scope=@careteen/create-react-app

藉助cross-spawn開啓子進程安裝依賴

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做模板拷貝工作。

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代碼。

查看效果

運行下面腳本

npm run create -- myProject

可以在當前項目根目錄看到myProject的目錄結構。
copy-cra-result

此時已經實現了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

安裝依賴

# `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配置

"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文件

#!/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()編譯打包
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文件

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文件

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功能啓服務

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查看簡易實現。
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。使得安裝了新的依賴不再需要重新啓動項目也能正常運行。
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參數。

create-react-app myProject --use-pnp

在已有項目中開啓可使用yarn提供的--pnp

yarn --pnp
yarn add uuid

與此同時會自動在package.json中配置開啓pnp。而且不會生成node_modules目錄,取而代替生成.pnp.js文件。

{
  "installConfig": {
    "pnp": true
  }
}

由於在開啓了 PnP 的項目中不再有 node_modules 目錄,所有的依賴引用都必須由 .pnp.js 中的 resolver 處理
因此不論是執行 script 還是用 node 直接執行一個 JS 文件,都必須經由 Yarn 處理

{
  // 還需配置使用腳本
  "scripts": {
    "build": "node uuid.js"
  }
}

運行腳本查看效果

yarn run build
# 或者使用node
yarn node uuid.js

pnp

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路徑之外,會拋錯。
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文件進行正則替換。

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兩個字段對丟失的依賴重新安裝。

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
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.