用工具沉淀技术积累

986 阅读9分钟
原文链接: zhuanlan.zhihu.com

大部分团队都或多或少在实际的项目中有技术的积累,这其中包括基础技术能力的积累,也包括了与产品、项目紧密结合的最佳实践的积累。

在不同的团队中,有不同的沉淀积累的方式,有文档,有串讲,更有口口相传。而在我们的团队,我们选择将知识沉淀成工具,以最快捷的方式传播最佳实践。而最终成型的,就是我们的内部工具ee-fe-tools

背景

回顾团队内的产品,大约8个产品,其中除了1个有着祖传jQuery实现的部分,另一个有着祖传Angular1实现的部分外,大部分产品都是一致的React + Webpack [+ Redux]的形式,团队的成员也有着相对一致的技术栈知识,这是我们产生统一工具的基本起点。

在统一工具之前,我们发现,无论多么熟练的工程师,在上述的技术选型之个,额外加上antd等一堆东西,从0开始启动一个新的系统也不得不花上2-3天的时间,这其中包含了大量的配置型工作,包括NPM基本配置、Lint工具、Git Hook、Webpack等等。而如果有一个统一的工具可以协助这些,则能非常有效地将新系统的启动时间降到小时级别。

Lint

Lint是任何项目中非常重要的一环,因此我们的工具提供了et lint的功能。

这一功能是对eslintstylelint的封装,内置了430+条规则来检测所有的JavaScript和LESS文件。在进行封装后,所有的eslint-plugin-*babel-eslintstylelint-config-*等等都不再被项目直接依赖,只需要一个简单的et lint命令,就能看到:

一天好心情(个鬼)

Lint部分的实现相对简单,其中包括的重点是:

  • 配置好合理的Cache位置,eslint默认在当前目录下放一个.eslintcache文件,这就会需要使用方来改动.gitignore以避免缓存文件被上传到仓库中。因此我们的工具将缓存目录修改到了node_modules/.cache/.eslintcache中。
  • 两个工具生成的结果的对象结构略有不同,为了更好地显示在命令行中,编写了一些逻辑将格式进行统一。最终使用eslint-formatter-pretty来进行格式化,这个格式很友好地在文件名后添加了行号,如index.js:9:7,直接点击能让vscode等编辑器快速定位到具体的行上。
  • stylelint-webpack-plugin存在一个BUG,当前无法在同时开启emitErrors和failOnError的情况下看到错误信息

除此之外,在开发的过程中,我们也协助stylelint修复了一个BUG,同时为stylelint-webpack-plugin的BUG提供了一些修复思路。

当然Lint部分还--changed--staged参数来控制范围,对于包含着大量的难以修复规范问题的祖传代码的项目,配合husky也能取得较好的效果。考虑到有Partially Staged这种情况存在,我们没有推荐在Git Hook中使用--fixgit add指令自动修复问题。

我们将我们的Lint配置开放了出来:

构建

既然社区都已经有了Webpack工程师这个神奇的称号,其配置在我们的项目中也是一项艰巨的挑战。

大多数的团队会使用复制粘贴,或者直接提供一个基础配置的NPM包的形式来加速Webpack的配置过程,鲜有使用工具的形式来封装的情况。UmiJS是一个可行的选择,但构建的配置事实上代表着一种开发的约定,而我们的项目在长期的开发过程中,有自己一套固有的开发形式与约定,对此进行迁移,或fork了UmiJS进行定制,都是不小的成本。考虑到我个人对Webpack还算熟悉(被逼的),自己重新进行一套封装反而是比较直接的选择。

在构建上的工作量与Lint不可同日而语,简单来说,我们实现了:

  • 将复杂的Webpack配置变成一个简单的settings.js文件,使用约10个属性来区别不同项目的特殊性。
  • 为了应对Angular1或者jQuery这样的场景,允许项目自身再额外增加一些配置。
  • 基于统一的技术栈,封装了css-modulespostcss等12个loader,importadd-react-displaynamereact-requirereact-transformreact-css-modules等9个babel plugin。
  • 基于对文件名的约定,实现了LESS变量的自动导入(src/styles/*.var.less),区分全局与CSS Modules的样式(*.global.less),区分Web Worker与普通模块(*.worker.js)等。
  • process.env.XXX及其它一些关键内容统一到了DefinePlugin中。
  • 为CSS Modules提供一个更漂亮的类名的模板(是的就是这么蛋疼,为了好看不异一切代价),且同时在css-loaderbabel-plugin-react-css-modules中生效。
  • 为项目提供了基于Feature Matrix的构建功能
  • 提供默认的splitChunks配置,项目在无配置的情况下可默认拆分为infrastructureuichartvendorsapp等多个Chunk,对于我们内部且相对重型的系统而言,可以有效地利用HTTP的链接数,并且使得前3个chunk的hash更为稳定,减少缓存失效的影响。
  • 使用webpack-bundle-analyzerunused-files-webpack-plugin对构建结果进行分析。
  • 支持第三方包类型的项目构建,使得工具也同时用于我们自己的UI库等的构建工作。
  • 提供versiontimemodetarget等一系列构建相关的全局变量,以便在采集数据、排查问题时使用。
  • 生成默认的HTML文件模板,除基本结构外,包含了构建的版本号等关键信息,以供线上排查问题。

默认的Chunk拆分让系统能第一时间享受到一个体积平衡、充分利用链接数据的配置:

看看这曼妙身材(个鬼)

为了隐藏起所有的这些配置,我们将各种依赖从项目中移到了我们的工具里,这导致在实际构建过程中,普通的写法如:

// babel.config.js
{
    presets: ['env']
}

是无效的,这会导致从项目的node_modules来查找,而实际上babel-preset-env的真正位置是node_modules/@baidu/ee-fe-tools/node_modules/babel-preset-env

因此,在整个配置中,大量使用了require.resolve来确定实际的路径。唯一的遗憾是babel-plugin-react-css-modules官方不支持其运行时的辅助函数使用上述形式,也就是说如果一个项目使用styleName,则依旧必须安装这个插件,非常的丑陋。

同时babel-plugin-react-css-modules还带来了另一个问题,在此摘录代码进行说明:

// 由于构建等是通过`ee-fe-tools`包进行的,因此在这个过程中所有`require.resolve`调用都会在这个包下开始查找,
// 而`babel-plugin-react-css-modules`是会调用`require.resolve`来分析相应的样式表依赖,这会导致找不到文件
//
// ```
// Cannot find module 'reset-css/reset.less'
// ```
//
// 为了让整个`require.resolve`过程从项目(不是这个包)的`node_modules`下开始找,需要`module-resolver`这个插件,
// 但是这个插件如果应用于所有的模块,则会影响其它插件的工作(比如`babel-plugin-import`完全失效),
// 因此配置该插件仅对**非相对路径的样式文件**生效
[
    require.resolve('babel-plugin-module-resolver'),
    {
        resolvePath(sourcePath) {
            if (sourcePath.startsWith('.')) {
                return null;
            }

            const extension = path.extname(sourcePath);
            if (extension === '.less' || extension === '.css') {
                // 使用与webpack的`resolve.modules`一样的配置
                const paths = [path.join(cwd, 'src'), path.join(cwd, 'node_modules')];
                return require.resolve(sourcePath, {paths});
            }

            return null;
        }
    }
]

这致使我们使用了Node 8.9.0以后才有的一个API(require.resolvepaths选项),好在作为公司的工程效率部门,我们对Node构建的环境有绝对的掌控权,这不成问题。

而以上这些所产生的最终结果,是项目只要写一个settings.js,然后运行et build,在最少配置3-4个选项,最多也就配置10个选项的情况下,就可以完成整个项目的构建,还直接享受了各种优化的效果,在不同项目中都得到了可观的效果:

开发调试

在有了构建的基础后,开发调试并不是一个难题,它仅仅是对webpack-dev-server的一个封装。在这一环节上,我们提供了:

  • 与构建不同的配置,禁用掉压缩、拆Chunkt优化,调整Source Maps配置,提升构建速度。
  • 提供分级的HMR,依据项目的新旧以及技术实施的严格程度,可以仅对样式进行HMR,或连同组件一起形成全局的HMR。
  • 提供了对公司内部测试环境的直接登录能力,在配置中声明presetUsers,启动调试服务器后就会自动跳转至一个页面,选择用户就自动登录。
  • 构建完成后自动打开浏览器页面,且在重构建时不会自动打开(与devServer.open略有不同,我们是在构建成功后才实际打开)。

webpack-dev-server的封装的麻烦之处在于,原本webpack-dev-server会自动给webpack配置的entry字段注入自己的运行时,但由于现在是通过程序的形式调用,就没有了这一功能。在经过一系列搜索后,我们发现可以这样子来完成:

const devServer = require('webpack-dev-server');
devServer.addDevServerEntrypoints(config, devServerConfig);

在进行了工具的封装后,只需要et dev即可打开调试服务器,等待浏览器打开新的标签,随后点击一个预设的用户,即可进入调试。

我们有尝试在这个过程中增加DLL的功能,但并未带来什么提升(甚至拖了速度的后腿),因此在搞清楚具体的最优化DLL配置之前,当前并没有内置DLL的能力。

效果总结

我们发现,在提供了基础的工具后,我们的新项目启动时间可以压至2小时以内。

这2小时的工作主要包含了建库、npm init、生成基础的目录结构、引入工具并配置scripts等。在后续我们进一步提供初始代码库的模板,后有望将时间进一步压缩至半小时以内。

而对于未来,我们期待着工具可以提供更多的能力,比如:

  • 基本的脚手架代码生成。
  • 在必要的情况下ExtractCSS的能力。
  • 自动上传产出到CDN的功能。

考虑到工具统一的第一步已经走出,后续分散的维护和优化变为了集中对一个代码库的维护,我们可以想象得到一个非常远大的前景。

以下是我们的工具的宣传资料:

使用工具沉淀技术.pdfpan.baidu.com