说到前端编译方案,也就是如何打包项目,如何编译组件,可选方案有不少,好比:css
若是你喜欢零配置的 parcel,那么项目和组件均可以拿它来编译。前端
若是你业务比较复杂,须要使用 webpack 作深度定制,那么常见组合是:项目 - webpack,组件 - gulp。node
但项目与组件的编译存在异同点,不一样构建工具支持的生态也存在异同点。react
type="module"
)。项目构建的目的主要在于发布 CDN,因此你们通常不在意构建脚本的通用性。换句话说,不管项目使用了怎样的构建方式,怎样理解 import
语句,甚至写出 require.context
等自定义语法,只要最终编译出符合浏览器规范的代码(考虑到兼容性)就足够。webpack
组件构建的目的主要在于发布 NPM,除了 ESNext 规范会使用 Babel 编译成 ES3,大部分代码写的很收敛,甚至对 SASS 的使用都要与 Typescript 插件一块儿组合成复杂的 Gulp Task。git
因此每每你们会对项目采起复杂的构建约束策略,而对组件的编译采起相对简单的办法,确保发布代码的通用性。github
因此在大部分项目使用 webpack 支持 worker-loader 时,编写组件时发现这段代码不灵了。或者至少你得付出一些代价,由于组件的调试依然能够利用 webpack-dev-server,这时能够加上 worker-loader,但因为 gulp 没有靠谱的 worker 插件,你的组件可能须要将 Worker 引用部分原样输出,但愿由引用它的项目作掉对 worker-loader 的支持。web
其实这种心态是很危险的,不只致使了组件不通用,甚至引起了各构建工具的 Tree Shaking 优化。缘由就是构建组件的代码太原始,冗余的代码没有删除,甚至直接引用的 SASS 代码仍然保留,更危险的是带上了一些特殊 webpack loader 才支持的语法。typescript
之因此说 Antd 是一个拥有优秀基因的前端组件库,是由于他遵循了前端组件最基本的代码素养:gulp
因此一个 靠谱的组件库 的产出文件,应该符合基本 ES 模块化规范,且不包括任何特殊语法。
可是这引起了一个新的问题:组件开发体验比项目差不少。
好比组件想使用雪碧图自动优化、想使用 worker-loader 方便快捷的调用多线程,想用本身的 css modules,甚至想把项目里一堆 PostCSS 快捷语法搬过来时怎么办?难道组件开发就不能得到与项目开发同样的体验吗?
要解决这个问题,笔者介绍一种基于 webpack 的通用构建方案,让本地调试、CDN 打包、ES6 -> ES3 转换 都使用统一套配置代码,同一套 loader。
核心思想只有一句话:利用 webpack-node-externals 忽略 Webpack 对指向 node_modules 的 require 或 import 语句:
development
模式。production
模式。production
模式,且利用 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:
.container { border: 1px solid #ccc; } .primary { color: blue; &:hover { color: green; } }
entry.worker.ts:
import hello from "hello"; const ctx: Worker = self as any; ctx.onmessage = event => { ctx.postMessage(hello()); }; export default null as any;
entry.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
配置便可:
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
便可,对配置作以下修改:
export default { ...webpackConfig, mode: "production" };
组件发布时,依然使用 webpack-cli
构建,但利用 webpack-node-externals
忽略对 node_modules
的解析。
import * as nodeExternals from "webpack-node-externals"; export default { ...webpackConfig, mode: "production", externals: [nodeExternals()] };
此时编译的组件代码,包含了 Typescript 编译、SCSS 编译、css-modules 解析、worker-loader 解析,但全部 node_modules
代码都保持原样,好比下面的代码:
作了代码去重、按需加载、打包、压缩,但由于保持了 require
原样,所以大小只有源码体积。
同时上述三个场景都在复用 webpack 一套代码的基础上,利用了 webpack 的生态,所以维护性和拓展性都很强。后续再加入新功能,不再须要处处找 babel
或 gulp
的插件了!
本文从 webpack
为切入点,但其实还能够从 parcel
或 gulp
为切入点,实现前端项目、组件构建体系的统一。
不过从可定制性来看,webpack
插件生态更完善,因此笔者选择了 webpack
。
留下一个思考题:你的项目、组件是如何构建的呢?是用了一套代码,仍是两套呢?
讨论地址是: 精读《如何编译前端项目与组件》 · Issue #125 · dt-fe/weekly
若是你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。