前言
大家好,我是Fly哥,继续上一次 搭建 monorepo 仓库发文已经 很久了, 这是工程化系列的第二篇, 不熟悉的同学可以看下上一篇文章10分钟带你从0到1搭建monorepo 工程化项目 这一篇文章是延续上一篇文章, 从0-1带你搭建 大型 react 项目。读完本篇文章你可以学到如下, 如果你会了,直接跳过别浪费时间,预计阅读 15分钟。
- tsx 的 编译的两种方式 babel 和 ts-loader
- 基于webpack 5 的 前端构建策略
安装webpack
我们先在项目中 安装 webpack 和 react 和 react-dom
yarn add -D -W webpack webpack-cli yarn add react react-dom
然后我们在项目 中定义一个 文件 webpack.dev.js 然后我们在package.json 定义一个
开发环境的打包脚本
"dev": "webpack serve --config ./scripts/webpack.dev.js"
这段脚本的意思就是 webpack 的打包 方式通过 配置文件 所以 我们 定义一下我们
webpack 的配置。 webpack的配置文件 默认是 commonjs 的导出方式 , 如果是用
esm 的方式 是不可以的, 但是如果你是用 cli , 可以都用 esm 的方式 。
mode: 'development', devtool: 'inline-source-map', entry: path.join(rootPath, '3d/src/index.tsx'), output: { filename: 'bundle.js', clean: true, // 每次打包之前 清空 dist目录 },
我们定义了 入口 文件 ,以及出口文件, clean: true 在webpack 5 我们不要安装插件
了 去清除每一次的dist 目录,webpack 5 默认都是支持的 。这样我们直接执行 去打包 是 会直接报错的。
原因:「webpack 他不认识 以 tsx 结尾的文件」, 所以就需要我们经常 所说的 loader , loader 的作用 就是 将 tsx 结尾的文件, 编译webpack 能够 认识的 文件 也就是 js。
社区中对与 tsx 的编译方式 其实 有2种
- 第一种 使用 ts-loader 去进行 tsx 的 打包
- 第二种 就是 使用 babel 对我们 对于我们的 tsx 进行打包
- 第三种 就是 ts-laoder 和 babel-loader 的结合
ts-loader
这里的话我们先用ts-loader 进行打包 首先先安装
yarn add -D -w ts-loader
然后我们进行以下配置:
{ test: /\.tsx?$/, use: [ { loader: 'ts-loader', options: { transpileOnly: true, happyPackMode: true, configFile: join(rootDir, 'tsconfig.json'), experimentalWatchApi: true, getCustomTransformers: join(__dirname, 'ts-transformers'), }, }, ], exclude: /node_modules/, },
「transpileOnly:」 表示 我们只进行编译不进行 类型检查, 不然项目越大,开发环境体验太差了。如果你需要,进行类型检查, 可以 配合这个webpack 插件 进行 使用
yarn add -D -W fork-ts-checker-webpack-plugin
然后我们在webpack 进行配置如下:
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') plugins: [ new ForkTsCheckerWebpackPlugin(), ],
「HappyPackMode:」 表示开启多进程, 或者 可以使用 thread-loader 都可以
「configFile」:你项目ts-config.json 的文件 , 我们这里就是跟目录的 ts 配置文件
「getCustomTransformers:」 这个其实就是 自定义一些 转换tsx ,举个常见的例子, 就是 ts-loader 如何 实现 按需加载, 下面的文章内容我使用babel 配和 babel-plugin-import 实现了, 但是「如果你使用 ts-loader 进行 如何实现按需加载 ???」
我们看下这个接口类型:
(program: Program, getProgram: () => Program) => { before?: TransformerFactory<SourceFile>[]; after?: TransformerFactory<SourceFile>[]; afterDeclarations?: TransformerFactory<SourceFile>[]; }
按需加载 其实就是对编译 tsx 的 过程中 ,给我们 提供了 3个钩子, 而按需加载, 对于 生成的 AST 语法树 进行改写, 转换成 按需加载的引用方式, 而你 自己根本不需要关心。社区里面已经有轮子, 替我们搞定了 。
yarn add ts-import-plugin -D -W
webpack 进行以下配置
getCustomTransformers: () => ({ before: [ tsImportPluginFactory( [{ libraryName:'antd', libraryDirectory:'lib', style:true }]) ] }),
其实和 babel-plugin-import 整体使用的方式差不多的。
如果项目中使用 babel 进行编译, 那就 以babel 的方式 去编译 tsx ,这两个其实 都可以 ,但是 个人倾向于 babel 进行编译, 毕竟可扩展性更强, 你可以编写babel 插件 ,或者一些新的语法。下面我们就来介绍使用babel 进行编译
babel
这时候我们用「babel-loader」 的方式 去对 tsx 进行打包。
我们先安装下面几个包
yarn add -D -W @babel/core @babel/preset-env babel-loader @babel/plugin-transform-runtime @babel/preset-react babel-loader
我一个个去解释每一个包是干什么用的
babel-loader
babel-loader 首先对于我们项目中的jsx 文件, 需要通过「jsx」 文件转换为「js」 文件, 但是「babel-loader」 这里起到的作用可能就是转换器。
@babel/core
但是babel-loader
仅仅识别出了jsx
文件,内部核心转译功能需要@babel/core
这个核心库,@babel/core
模块就是负责内部核心转译实现的。
@babel/preset-env
@babel/preset-env
是一个智能预设,允许您使用最新的 JavaScript,而无需微观管理目标环境需要哪些语法转换(以及可选的浏览器 polyfill)。这既让你的生活更轻松,也让 JavaScript 包更小!
@babel/prest-env
是babel
转译过程中的一些预设,它负责将一些基础的es 6+
语法,比如const/let...
转译成为浏览器可以识别的低级别兼容性语法。同时在打包的过程中, 通过配置进行按需引用, 后面参数我会讲解,但是一些高级别模块
(polyfill
)的实现还是不支持的,这时候我们就需要 下面一个 npm 包
@babel/plugin-transform-runtime
@babel/plugin-transform-runtime
,上边我们提到了对于一些高版本内置模块,比如Promise/Generate
等等@babel/preset-env
并不会转化,所以@babel/plugin-transform-runtime
就是帮助我们来实现这样的效果的,他会在我们项目中如果使用到了Promise
之类的模块之后去实现一个低版本浏览器的polyfill
。
@babel/preset-react
我们希望将.jsx
文件转化为js
文件同时将jsx
标签转化为React.createElement
的形式,此时我们就需要额外使用babel
的另一个插件-@babel/preset-react
@babel/preset-typescript
babel
内置了一组预设去转译TypeScript
代码 --@babel/preset-typescript
。接下来,我们进行babel 的配置呗,
{ // 同时认识ts jsx js tsx 文件 test: /\.(t|j)sx?$/, use: { loader: 'babel-loader', options: { presets: [ [ '@babel/preset-env', { useBuiltIns: 'usage', corejs: 3, }, ], '@babel/preset-react', '@babel/preset-typescript', ], plugins: [ [ '@babel/plugin-transform-runtime', { regenerator: true, }, ], ], }, }, },
这里主要给大家介绍下这两个参数
- 「useBuiltIns」:无论你选择 usage 还是 entry , babel 都依赖 core-js 这个 包,因为 @babel/polyfill 已经被废弃, 默认是core-js 2.0 版本, 你可以选择3.0版本
yarn add core-js@2 -D -W || yarn add core-js@3 -D -W
我们看下我们的测试文件:
import ReactDom from 'react-dom' class A { static async wait() { return Promise.resolve(5) } ctx?: number constructor(ctx?: number) { this.ctx = ctx ?? 5 } } ReactDom.render(<div>{`${new A('2').ctx}我是测试的`}</div>, document.getElementById('root'))
然后我们执行 打包命令:yarn build
suc
但是用babel-loader 打包tsx 文件的时候, 做不到类型检查, 细心的同学可以发现 我传一个了字符串 进去, 按道理我们在打包之前就应该做一次类型检查。这时候有同学就会问, babel 支持ts 的类型检查???
「答案是不可以」
因为 tsc 的类型检查是需要拿到整个工程的类型信息,需要做类型的引入、多个文件的 namespace、enum、interface 等的合并,而 babel 是单个文件编译的,不会解析其他文件的信息。所以做不到和 tsc 一样的类型检查。
「一个是在编译过程中解析多个文件,一个是编译过程只针对单个文件,流程上的不同,导致 babel 无法做 tsc 的类型检查。」
那我们怎么去解决呢或者去限制呢
1.我们可以使用 tsc的类型检查 但是不去进行编译, 我们用 tsc + babel 的方式去进行打包
"build": "yarn check && webpack --config ./scripts/webpack.base.ts", "check": "tsc --noEmit",
2.我们使用webpack插件 去做类型检查
yarn add fork-ts-checker-webpack-plugin -D -W
然后我们在插件处进行配置。
webpack
然后我们执行build
error
检查出错误了。
monorepo 项目babel配置
这里做一个小提醒, 在monorepo 项目中 如果想将babel 的配置提取出来,我一开始创建的是 .babelrc 文件 然后把配置放进去, 经过查阅官方资料, 对于 monorepo 项目 更加推荐在项目
任何 monorepo 结构的第一步应该是babel.config.json
在存储库根目录中创建一个文件。这确立了 Babel 的核心概念,即存储库的基本目录。即使您想使
用.babelrc.json
文件来配置每个单独的包,重要的是要有一个存储库级别选项的位置。
您通常可以将所有 repo 配置放在 root 中babel.config.json
。使用"overrides".babelrc.json
,您可以轻松地指定仅适用于存储库的某些子文件夹的配置,这通常比跨存储库创建许多文件更容易遵循。
您可能会遇到的第一个问题是,默认情况下,Babel 期望babel.config.json
从设置为其"root"的目录加载文件,这意味着如果您创建一个babel.config.json
,但在单个包中运行 Babel,例如
cd packages/some-package; babel src -d dist
在该上下文中使用的“根” Babel不是您的 monorepo 根,它无法找到该babel.config.json
文件。
如果你的所有构建脚本都相对于你的存储库根目录运行,那么一切应该已经工作了,但是如果你是从一个子包中运行你的 Babel 编译过程,你需要告诉 Babel 在哪里寻找配置。有几种方法可以做到这一点,但推荐的方法是使用"rootMode"选项"upward"
,这将使 Babel 从工作目录向上搜索您的babel.config.json
文件,并将其位置用作"root"值。
babel缓存配置
首先在babel-loader 后面加上 ?cacheDirectory , 缓存每一个babel转译的结果
{ // 同时认识ts jsx js tsx 文件 test: /\.(t|j)sx?$/, exclude: /node_modules/, use: { loader: 'babel-loader?cacheDirectory', }, },
同时在babel.config.js 增加 如下代码
module.exports = function (api) { // 这里 我们根据api 做一些自定义的webpack 打包配置, 比如根据环境 // 根据node 和 web 做一些不同的东西 return { presets: [ [ '@babel/preset-env', { useBuiltIns: 'usage', corejs: 3, // caller.target 等于 webpack 配置的 target 选项 targets: api.caller((caller) => caller && caller.target === 'node') ? { node: 'current' } : { chrome: '58', ie: '11' }, }, ], [ '@babel/preset-typescript', { isTSX: true, allExtensions: true, }, ], '@babel/preset-react', ], plugins: [ [ '@babel/plugin-transform-runtime', { regenerator: true, }, ], ], ignore: ['dist', 'node_modules'], } }
资源配置
资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。
在 webpack 5 之前,通常使用:
raw-loader
将文件导入为字符串
url-loader
将文件作为 data URI 内联到 bundle 中
file-loader
将文件发送到输出目录
资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
asset/resource
发送一个单独的文件并导出 URL。之前通过使用file-loader
实现。
asset/inline
导出一个资源的 data URI。之前通过使用url-loader
实现。
asset/source
导出资源的源代码。之前通过使用raw-loader
实现。
asset
在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用url-loader
,并且配置资源体积限制实现。
当在 webpack 5 中使用旧的 assets loader(如 file-loader
/url-loader
/raw-loader
等)和 asset 模块时,你可能想停止当前 asset 模块的处理,并再次启动处理,这可能会导致 asset 重复,你可以通过将 asset 模块的类型设置为
'javascript/auto'
来解决。
然后我的 webpack 配置如下
{ test: /\.(png|jpg|gif|svg)$/, type: 'asset/resource', }, { test: /\.(ttf|eot|woff|woff2)$/i, type: 'asset/resource', generator: { filename: 'assets/[name].[contenthash:8].[ext]', }, }, { test: /\.txt$/, type: 'asset/source', },
然后我们在项目中重新引入 一张图片 会出现下面这种情况
image-20220502171617345
其实就tsx, 找不到 这个模块, 我们在 monorepo 根目录 新建一个 types ,用来放置一些 全局的声明模块
image-20220502171829638
然后在里面加入
declare module '*.css' declare module '*.png' declare module '*.scss' declare module '*.svg' declare module '*.jpg' declare module '*.jpeg' declare module '*.gif' declare module '*.bmp' declare module '*.tiff'
这个时候还不行, 我们切换到子目录的 tsConfig 中, 然后在 include 文件 中 加入 当前 文件路径, 这样就不会报错了
{ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", "rootDir": "src" }, "include": ["./src/**/*", "../../types/index.d.ts"] }
这样就可以了, 就不会报类型错误了。
我们看一下打包生成的图片文件
image-20220502184002320
其实我们看了下主要在 dist 目录, 对于资源的配置我们其实也可以自定义图片的输出路径 有下面两种方法
- 第一个是在 webpack output 输出 配置下面下这段代码:
assetModuleFilename: 'images/[hash][ext][query]',
2 . 或者是在自定义输出目录如下:
{ test: /\.(png|jpg|gif|svg)$/, type: 'asset/resource', generator: { filename: 'images/[name].[contenthash:8][ext]', }, },
这两种方式其实是等效的,但是自定义资源目录。但是第二种方式仅适用于 asset
和 asset/resource
模块类型。
我们看下打包结果:
image-20220502184828504