-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
222 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
# 1 引言 | ||
|
||
说到前端编译方案,也就是如何打包项目,如何编译组件,可选方案有很多,比如: | ||
|
||
- 通过 webpack / parcel / gulp 构建项目。 | ||
- 通过 parcel / gulp / babel 构建组件。 | ||
|
||
如果你喜欢零配置的 parcel,那么项目和组件都可以拿它来编译。 | ||
|
||
如果你业务比较复杂,需要使用 webpack 做深度定制,那么常见组合是:项目 - webpack,组件 - gulp。 | ||
|
||
但项目与组件的编译存在异同点,不同构建工具支持的生态也存在异同点。 | ||
|
||
## webpack parcel gulp 生态的区别 | ||
|
||
- babel 一般不会解析模块,也就是一般仅做代码预处理,而不会改变文件结构,也对 require、import 语句不敏感。 | ||
- webpack / parcel 主要就是解决模块化打包问题,因为浏览器还不支持(现在部分支持 `type="module"`)。 | ||
- gulp 理论上可以将 babel、webpack、parcel 作为插件,但这是后来的事。历史上由于 gulp 是作为 grunt 的替代品出现,当时要解决的问题是处理浏览器兼容问题,打包 scss 或 less,做一些公共资源替换,雪碧图等,最后可以顺带合并到一个文件,但模块化功能远远比 webpack 弱,基本上只能合并,但不能 “理解模块概念”。 | ||
|
||
## 项目构建与组件构建的区别 | ||
|
||
项目构建的目的主要在于发布 CDN,所以大家一般不在乎构建脚本的通用性。换句话说,无论项目使用了怎样的构建方式,怎样理解 `import` 语句,甚至写出 `require.context` 等自定义语法,只要最终编译出符合浏览器规范的代码(考虑到兼容性)就足够。 | ||
|
||
组件构建的目的主要在于发布 NPM,除了 ESNext 规范会使用 Babel 编译成 ES3,大部分代码写的很收敛,甚至对 SASS 的使用都要与 Typescript 插件一起组合成复杂的 Gulp Task。 | ||
|
||
**所以往往大家会对项目采取复杂的构建约束策略,而对组件的编译采取相对简单的办法,确保发布代码的通用性。** | ||
|
||
所以在大部分项目使用 webpack 支持 worker-loader 时,编写组件时发现这段代码不灵了。或者至少你得付出一些代价,因为组件的调试依然可以利用 webpack-dev-server,这时可以加上 worker-loader,但由于 gulp 没有靠谱的 worker 插件,你的组件可能需要将 Worker 引用部分原样输出,希望由引用它的项目做掉对 worker-loader 的支持。 | ||
|
||
其实这种心态是很危险的,不仅导致了组件不通用,甚至引发了各构建工具的 Tree Shaking 优化。原因就是构建组件的代码太原始,冗余的代码没有删除,甚至直接引用的 SASS 代码仍然保留,更危险的是带上了一些特殊 webpack loader 才支持的语法。 | ||
|
||
之所以说 Antd 是一个拥有优秀基因的前端组件库,是因为他遵循了前端组件最基本的代码素养: | ||
|
||
1. 编译后的代码全部符合基本 JS 规范,换个角度来说,使用 webpack 内置基本 js loader 就能完全解析。 | ||
2. 将 css 代码抽离出来,这样不会强制项目对 node_modules 的代码应用 css-loader。 | ||
|
||
所以一个 **靠谱的组件库** 的产出文件,应该符合基本 ES 模块化规范,且不包括任何特殊语法。 | ||
|
||
但是这引发了一个新的问题:组件开发体验比项目差很多。 | ||
|
||
比如组件想使用雪碧图自动优化、想使用 worker-loader 方便快捷的调用多线程,想用自己的 css modules,甚至想把项目里一堆 PostCSS 快捷语法搬过来时怎么办?难道组件开发就不能获得与项目开发一样的体验吗? | ||
|
||
要解决这个问题,笔者介绍一种基于 webpack 的通用构建方案,让本地调试、CDN 打包、ES6 -> ES3 转换 都使用统一套配置代码,同一套 loader。 | ||
|
||
# 2 精读 | ||
|
||
核心思想只有一句话:利用 [webpack-node-externals](https://github.com/liady/webpack-node-externals) 忽略 Webpack 对指向 node_modules 的 require 或 import 语句: | ||
|
||
1. 进行项目/组件调试时,开启 `development` 模式。 | ||
2. 进行项目编译时,开启 `production` 模式。 | ||
3. 进行组件编译时,开启 `production` 模式,且利用 [webpack-node-externals](https://github.com/liady/webpack-node-externals) 插件忽略 node_modules。 | ||
|
||
可以想像,根据第三条,如果所有组件都按照这个模式输出代码,那么 webpack 对 node_modules 编译时,只需要将所有 `require` 代码进行合并,不需要执行任何 loader,也不需要压缩,不需要 TreeShaking,因为这些在组件代码编译时全部已经做好了,这种构建效率几乎达到最大。 | ||
|
||
## 实际案例 | ||
|
||
我们拿支持 `typescript`、`sass`、`css-modules`、`worker-loader` 的场景作为案例。 | ||
|
||
我们创建三个文件 `entry.tsx` `entry.worker.ts` 与 `entry.scss`: | ||
|
||
**entry.scss:** | ||
|
||
```scss | ||
.container { | ||
border: 1px solid #ccc; | ||
} | ||
|
||
.primary { | ||
color: blue; | ||
&:hover { | ||
color: green; | ||
} | ||
} | ||
``` | ||
|
||
**entry.worker.ts:** | ||
|
||
```tsx | ||
import hello from "hello"; | ||
|
||
const ctx: Worker = self as any; | ||
|
||
ctx.onmessage = event => { | ||
ctx.postMessage(hello()); | ||
}; | ||
|
||
export default null as any; | ||
``` | ||
|
||
**entry.tsx:** | ||
|
||
```tsx | ||
import * as React from "react"; | ||
import styles from "./entry.scss"; | ||
import * as MyWorker from "./parser.worker"; | ||
|
||
const worker = new MyWorker(); | ||
|
||
export default () => ( | ||
<div className={styles.container}> | ||
<button className={styles.primary}>Click Me.</button> | ||
</div> | ||
); | ||
``` | ||
|
||
在上面三个文件中,我们分别利用了 Typescript 编译、SCSS 编译、css-modules 解析、worker-loader 解析(利用 webpack 自动生成字符串代码并利用 Blob URL 方式载入,这样就不需要创建新文件也可以用 worker 了,也不会存在跨域问题)。 | ||
|
||
为了支持这几个特性对如上代码做调试、项目发布、组件发布,我们分别看下这三个场景该如何配置编译脚本。 | ||
|
||
### 本地调试 | ||
|
||
本地调试是不用区分组件与项目的。因为无论何种情况,都需要进行基本的项目编译,载入所有自定义 loader 并打成一个 bundle 包。 | ||
|
||
此时我们只要维护一份 `webpack` 配置即可: | ||
|
||
```typescript | ||
const webpackConfig = { | ||
mode: "development", | ||
module: { | ||
rules: [ | ||
{ | ||
test: /\.worker\.tsx?$/, | ||
use: { | ||
loader: "worker-loader", | ||
options: { | ||
inline: true | ||
} | ||
}, | ||
include: path.join(projectRootPath, "src") | ||
}, | ||
{ | ||
test: /\.tsx?$/, | ||
use: [ | ||
[ | ||
"babel-loader", | ||
{ | ||
plugins: [ | ||
[ | ||
"babel-plugin-react-css-modules", | ||
{ | ||
filetypes: { | ||
".scss": { | ||
syntax: "postcss-scss" | ||
} | ||
} | ||
} | ||
] | ||
] | ||
} | ||
], | ||
"ts-loader" | ||
], | ||
include: path.join(projectRootPath, "src") | ||
}, | ||
{ | ||
test: /\.scss$/, | ||
use: [ | ||
"style-loader", | ||
[ | ||
"css-loader", | ||
{ | ||
importLoaders: 1, | ||
modules: true | ||
} | ||
], | ||
"sass-loader" | ||
], | ||
include: path.join(projectRootPath, "src") | ||
} | ||
] | ||
} | ||
}; | ||
|
||
export default webpackConfig; | ||
``` | ||
|
||
利用这个配置加上 `webpack-dev-server` 即可完成组件与项目的本地调试。 | ||
|
||
### 项目发布 | ||
|
||
项目发布时,需要将所有代码打入到一个 bundle 包,此时只需使用 `webpack-cli` 即可,对配置做如下修改: | ||
|
||
```tsx | ||
export default { | ||
...webpackConfig, | ||
mode: "production" | ||
}; | ||
``` | ||
|
||
### 组件发布 | ||
|
||
组件发布时,依然使用 `webpack-cli` 构建,但利用 `webpack-node-externals` 忽略对 `node_modules` 的解析。 | ||
|
||
```tsx | ||
import * as nodeExternals from "webpack-node-externals"; | ||
|
||
export default { | ||
...webpackConfig, | ||
mode: "production", | ||
externals: [nodeExternals()] | ||
}; | ||
``` | ||
|
||
此时编译的组件代码,包含了 Typescript 编译、SCSS 编译、css-modules 解析、worker-loader 解析,但所有 `node_modules` 代码都保持原样,比如下面的代码: | ||
|
||
<img width=500 src="https://cdn.nlark.com/lark/0/2019/png/29349/1547821898158-3554f84e-27f7-4cc4-bb47-3f49803e6837.png?OSSAccessKeyId=LTAI1M4etAl6H5AN&Expires=1547823703&Signature=sEHFGvC3YW9F%2BvKcMtVlhJ0a5gM%3D"> | ||
|
||
做了代码去重、按需加载、打包、压缩,但因为保持了 `require` 原样,因此大小只有源码体积。 | ||
|
||
同时上述三个场景都在复用 webpack 一套代码的基础上,利用了 webpack 的生态,因此维护性和拓展性都很强。后续再加入新功能,再也不需要到处找 `babel` 或 `gulp` 的插件了! | ||
|
||
# 3 总结 | ||
|
||
本文从 `webpack` 为切入点,但其实还可以从 `parcel` 或 `gulp` 为切入点,实现前端项目、组件构建体系的统一。 | ||
|
||
不过从可定制性来看,`webpack` 插件生态更完善,所以笔者选择了 `webpack`。 | ||
|
||
留下一个思考题:你的项目、组件是如何构建的呢?是用了一套代码,还是两套呢? | ||
|
||
> 讨论地址是:[精读《如何编译前端项目与组件》 · Issue #125 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/125) | ||
**如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** |