Molecule 在构建工具中的选择
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。
本文作者:修能
朝闻道,夕死可矣
何为 Molecule?
轻量级的 Web IDE UI 框架——Molecule
我们开源了一个轻量的 Web IDE UI 框架
Molecule实现数栈至简前端开发新体验
前言
构建通常指的是把源代码转换成发布到线上的可执行 JavaScrip、CSS、HTML 代码。在前端发展的过程中,源代码的模块体系在不断的更新,最终产物也在不断的更新。而随之也使得构建工具也在不断更新换代。
而目前来看,基于前端的细化领域下,针对不同领域下的构建工具也日新月异。来看看 Molecule 该如何选择构建工具呢?
Molecule 的需求
首先,我们需要分析 Molecule 对构建工具的需求有什么?
老版本的问题
- 本地开发和 build 的构建工具不同,不得不增加 web 命令来执行一个预览的任务,确保 build 后的产物没问题。
- 慢,由于使用 tsc 作为编译,所以编译较慢。
- 部分变量无法复用,导致重复定义。
代码编译
由于 Molecule 的代码是用 ESM 的模块书写,且 Molecule 面向的是 Web 应用。通常来说面向 Web 应用的依赖库是需要提供 ESM 的代码实现 tree shaking 的作用的。
所以我们这里需要把 ESM 书写的 Molecule 代码通过构建工具编译成 ESM。
思考:为什么要把 ESM 代码编译成 ESM?
- 将 TypeScript 编译成 JavaScript
- 将高级语法编译成低级语法
除此之外,由于我们考虑到 Node.js 后续发展以 Pure ESM 为主,且 Molecule 针对 CommonJS 的场景较少,故我们不考虑输出 CommonJS 的产物。
类型
需要支持输出类型。
样式
Molecule 中使用 BEM 作为类名规范,通常情况下使得需要在 Sass 中和 JavaScript 中都定义相同变量名。而类 Sass-in-JS 使得我们可以从 Sass 中导出变量名,在 JS 文件中使用。
这就使得构建工具不仅要支持 Sass 的编译,同时还需要支持插件,允许我们做 Sass-in-JS 的需求。
其他
其他相关文件,例如 JSON,PNG 等文件需要支持拷贝至相关指定目录。
调研构建工具
Webpack
Webpack 是目前构建工具中的老大哥了,作为顶级老牌构建工具,几乎所有场景都能适用。
缺点也仅仅是冗余代码较多,配置项太多,体积较大等。
Rollup
作为面向 JS 类库而出现的构建工具。其和 Webpack 相比,在打包后产生的冗余代码少,体积较小,功能专注。缺点仅仅是不支持 HMR。
Vite
直接排除
Parcel
Parcel 目前看作是面向 Web 应用的零配置,高速度的 Webpack。其有一个致命的弱点是,自定义插件支持不如 Webpack。这会让我们无法实现 Sass-in-JS。
2.0 可能有所改善,我不清楚。不予评价
swc
swc 在某种程度上,是 babel 和 tsc 的竞品,属于比较底层的构建工具。和 esbuild 同类型,只是 esbuild 基于 Go,swc 基于 Rust。
esbuild
extremely fast JavaScript Compiler
babel
很好,就是慢
tsc
很好,就是更慢。有一个优点,只有 tsc 能支持输出类型。
方案实施
由于大多数的构建工具都是 bundler,并不符合 Molecule 的定位。故采取的方案是 esbuild + Sass + tsc 的方案。
esbuild 取其作为 Compiler 的部分,Sass 取其编译 SCSS 文件的部分,tsc 负责编译出类型文件。
tsx 相关文件输出
transformCtx = await esbuild.context({
entryPoints,
bundle: false,
format: 'esm',
outdir: dist,
jsx: 'automatic',
plugins: [
{
name: 'alias',
setup(build) {
build.onLoad({ filter: /.*/ }, async (args) => {
const source = await fs.promises.readFile(args.path, 'utf8');
const contents = sassLoader(alias(source, args.path));
return {
contents,
loader: args.path.endsWith('.tsx') ? 'tsx' : 'ts',
};
});
},
},
],
});
await transformCtx.watch();
做两件事
- 别名重定位
- 将文件中的样式文件改为 css
样式文件输出
/**
*
* @param {string} entry
*/
async function _transform(entry) {
const res = await sass.compileAsync(entry);
const regex = /^:export {(\n|.)+}$/m;
const target = entry.replace(/src\//, 'esm/').replace(/.scss/, '.css');
const dirname = path.dirname(target);
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true });
}
const css = res.css.replace(regex, '');
fs.writeFileSync(target, css);
if (regex.test(res.css)) {
const exportModules = res.css.match(regex)[0];
fs.writeFileSync(
path.join(dirname, styleVariablesFileName),
exportModules
.replace(':export', 'export default')
.replace(/: .*;/gm, (substring) => {
const stringLiteral = /(?<="|')\S+(?="|')/g;
if (!stringLiteral.test(substring)) {
const startIdx = substring.indexOf(':');
const endIdx = substring.indexOf(';');
return `:"${substring.substring(startIdx + 1, endIdx).trim()}",`;
} else {
return substring.replace(';', ',');
}
})
);
}
}
做两件事
- 把
:export
干掉 - 把
:export
的内容放到当前目录下的style__variables.js
的目录中
类型文件输出
类型文件异步输出,防止阻塞
async function transformTyping() {
typingCtx = spawn('tsc && (concurrently "tsc -w" "tsc-alias -w")', {
stdio: 'inherit',
shell: true,
});
}
其他文件输出
/**
*
* @param {string} filePath
*/
function _copyFile(filePath) {
const dest = filePath.replace(/src\//, 'esm/');
const dirname = path.dirname(dest);
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true });
}
fs.createReadStream(filePath, 'utf-8').pipe(fs.createWriteStream(dest));
}
遗留问题
- 增量编译的问题
- 代码压缩
欢迎大家就以上问题留言讨论!
最后
欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star