⚠️ 本文为掘金社区首发签约文章,未获受权禁止转载css
去年同期写过一个基于 Node 的 DevOps 系列,可是整个项目工程很是大,上手成本比较高,对于一些中小型团队或者新手参考的意义不算多,因此针对这些群体重启了一个新的工程化系列。html
新的系列将从 0 到 1 逐步搭建一套完整工程化方案,全部文章将统一放在《前端工程化》专栏中。前端
先罗列一些小团队会大几率会遇到的问题:node
除了上述比较常见的几点外,其他的一些人为环境因素就不一一列举了,总结出来其实就是混乱 + 不舒服。react
同时处在这样的一个团队中,团队自身的规划就不明确,我的就更难对将来有一个清晰的规划与目标,容易所有陷于业务不可自拔、无限循环。webpack
当你处在一个混乱的环境,遇事不要慌(乱世出英雄,为何不能是你呢),先把事情捋顺,而后定个目标与规划,一步步走。web
上述列举的这些问题能够经过引入工程化体系来解决,那么什么是工程化呢?typescript
广义上,一切以提升效率、下降成本、保障质量为目的的手段,都属于工程化的范畴。npm
经过一系列的规范、流程、工具达到研发提效、自动化、保障质量、服务稳定、预警监控等等。json
对前端而言,在 Node 出现以后,能够借助于 Node 渗透到传统界面开发以外的领域,将研发链路延伸到整个 DevOps 中去,从而脱离“切图仔”成为前端工程师。
上图是一套简单的 DevOps 流程,技术难度与成本都比较适中,做为小型团队搭建工程化的起点,性价比极高。
在团队没有制定规则,也没有基础建设的时候,一般能够先从最基础的 CLI 工具开始而后切入到整个工程化的搭建。
因此先定一个小目标,完成一个团队、项目通用的 CLI 工具。
小团队里面的业务通常迭代比较快,能抽出来提供开发基建的时间与机会都比较少,为了不后期的重复工做,在作基础建设以前,必定要作好规划,思考一下当前最欠缺的核心与将来可能须要用到的功能是什么?
Coding 永远不是最难的,最难的是不知道能使用 code 去作些什么有价值的事情。
参考上述的 DevOps 流程,本系列先简单规划出 CLI 的四个大模块,后续若是有需求变更再说。
能够根据本身项目的实际状况去设计 CLI 工具,本系列仅提供一个技术架构参考。
一般在小团队中,构建流程都是在一套或者多套模板里面准备多环境配置文件,再使用 Webpack Or Rollup 之类的构建工具,经过 Shell 脚本或者其余操做去使用模板中预设的配置来构建项目,最后再进行部署之类的。
这的确是一个简单、通用的 CI/CD 流程,但问题来了,只要最后一步的发布配置不在可控以内,任意团队的开发成员均可以对发布的配置项作修改。
即便构建成功,也有可能会有一些不可预见的问题,好比 Webpack 的 mode 选择的是 dev 模式、没有对构建代码压缩混淆、没有注入一些全局统一方法等等,此时对生产环境而言是存在必定隐患的。
因此须要将构建配置、过程从项目模板中抽离出来,统一使用 CLI 来接管构建流程,再也不读取项目中的配置,而经过 CLI 使用统一配置(每一类项目均可以自定义一套标准构建配置
)进行构建。
避免出现业务开发同窗由于修改了错误配置而致使的生产问题。
与构建是同样的场景,业务开发的时候为了方便,不少时候一些通用的自动化测试以及一些常规的格式校验都会被忽略。好比每一个人开发的习惯不一样也会致使使用的 ESLINT 校验规则不一样,会对 ESLINT 的配置作一些额外的修改,这也是不可控的一个点。一个团队仍是使用同一套代码校验规则最好。
因此也能够将自动化测试、校验从项目中剥离,使用 CLI 接管,从而保证整个团队的某一类项目代码格式的统一性。
至于模板,基本上目前出现的博客中,只要是关于 CLI 的,就必然会有模板功能。
由于这个一个对团队来讲,快速、便捷初始化一个项目或者拉取代码片断是很是重要的,也是做为 CLI 工具来讲产出最高、收益最明显的功能模块,但本章就不作过多的介绍,放在后面模板的博文统一写。
既然是工具合集,那么能够放一些通用的工具类在里面,好比
前面介绍了 CLI 的几个模块功能设计,接下来能够正式进入开发对应的 CLI 工具的环节。
CLI 工具开发将使用 TS 做为开发语言,若是此时尚未接触过 TS 的同窗,恰好能够借此项目来熟悉一下 TS 的开发模式。
mkdir cli && cd cli // 建立仓库目录
npm init // 初始化 package.json
npm install -g typescript // 安装全局 TypeScript
tsc --init // 初始化 tsconfig.json
复制代码
全局安装完 TypeScript 以后,初始化 tsconfig.json 以后再进行修改配置,添加编译的文件夹与输出目录。
{
"compilerOptions": {
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"strict": true, /* Enable all strict type-checking options. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"include": [
"./src",
]
}
复制代码
上述是一份已经简化过的配置,但应对当前的开发已经足够了,后续有须要能够修改 TypeScript 的配置项。
由于是从 0 开发 CLI 工具,能够先从简单的功能入手,例如开发一个 Eslint 校验模块。
npm install eslint --save-dev // 安装 eslint 依赖
npx eslint --init // 初始化 eslint 配置
复制代码
直接使用 eslint --init
能够快速定制出适合本身项目的 ESlint 配置文件 .eslintrc.json
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:react/recommended",
"standard"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
}
}
复制代码
若是项目中已经有定义好的 ESlint,能够直接使用本身的配置文件,或者根据项目需求对初始化的配置进行增改。
第一步,对照文档 ESlint Node.js API,使用提供的 Node Api 直接调用 ESlint。
将前面生成的 .eslintrc.json 的配置项按需加入,同时使用 useEslintrc:false
禁止使用项目自己的 .eslintrc 配置,仅使用 CLI 提供的规则去校验项目代码。
import { ESLint } from 'eslint'
import { getCwdPath, countTime } from '../util'
// 1. Create an instance.
const eslint = new ESLint({
fix: true,
extensions: [".js", ".ts"],
useEslintrc: false,
overrideConfig: {
"env": {
"browser": true,
"es2021": true
},
"parser": getRePath("@typescript-eslint/parser"),
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint",
],
},
resolvePluginsRelativeTo: getDirPath('../../node_modules') // 指定 loader 加载路径
});
export const getEslint = async (path: string = 'src') => {
try {
countTime('Eslint 校验');
// 2. Lint files.
const results = await eslint.lintFiles([`${getCwdPath()}/${path}`]);
// 3. Modify the files with the fixed code.
await ESLint.outputFixes(results);
// 4. Format the results.
const formatter = await eslint.loadFormatter("stylish");
const resultText = formatter.format(results);
// 5. Output it.
if (resultText) {
console.log('请检查===》', resultText);
}
else {
console.log('完美!');
}
} catch (error) {
process.exitCode = 1;
console.error('error===>', error);
} finally {
countTime('Eslint 校验', false);
}
}
复制代码
npm install -g create-react-app // 全局安装 create-react-app
create-react-app test-cli // 建立测试 react 项目
复制代码
测试项目使用的是 create-react-app,固然你也能够选择其余框架或者已有项目都行,这里只是做为一个 demo,而且后期也还会再用到这个项目作测试。
新建 src/bin/index.ts
, demo 中使用 commander
来开发命令行工具。
#!/usr/bin/env node // 这个必须添加,指定 node 运行环境
import { Command } from 'commander';
const program = new Command();
import { getEslint } from '../eslint'
program
.version('0.1.0')
.description('start eslint and fix code')
.command('eslint')
.action((value) => {
getEslint()
})
program.parse(process.argv);
复制代码
修改 pageage.json,指定 bin 的运行 js(每一个命令所对应的可执行文件的位置)
"bin": {
"fe-cli": "/lib/bin/index.js"
},
复制代码
先运行 tsc
将 TS 代码编译成 js,再使用 npm link 挂载到全局,便可正常使用。
commander 的具体用法就不详细介绍了,基本上市面大部分的 CLI 工具都使用 commander 做为命令行工具开发,也都有这方面的介绍。
命令行进入刚刚的测试项目,直接输入命令 fe-cli eslint
,就能够正常使用 Eslint 插件,输出结果以下:
能够看出这个时候,提示并无那么显眼,可使用 chalk
插件来美化一下输出。
先将测试工程故意改错一个地方,再运行命令 fe-cli eslint
至此,已经完成了一个简单的 CLI 工具,对于 ESlint 的模块,能够根据本身的想法与规划定制更多的功能。
一般开发业务的时候,用的是 webpack 做为构建工具,那么 demo 也将使用 webpack 进行封装。
先命令行进入测试项目中执行命令 npm run eject
,暴露 webpack 配置项。
从上图暴露出来的配置项能够看出,CRA 的 webpack 配置仍是很是复杂的,毕竟是通用型的脚手架,针对各类优化配置都作了兼容,但目前 CRA 使用的仍是 webpack 4 来构建。做为一个新的开发项目,CLI 能够不背技术债务,直接选择 webpack 5 来构建项目。
通常来讲,构建工具替换不会影响业务代码,若是业务代码被构建工具绑架,建议仍是须要去优化一下代码了。
import path from "path"
const HtmlWebpackPlugin = require('html-webpack-plugin')
const postcssNormalize = require('postcss-normalize');
import { getCwdPath, getDirPath } from '../../util'
interface IWebpack {
mode?: "development" | "production" | "none";
entry: any
output: any
template: string
}
export default ({
mode,
entry,
output,
template
}: IWebpack) => {
return {
mode,
entry,
target: 'web',
output,
module: {
rules: [{
test: /\.(js|jsx)$/,
use: {
loader: getRePath('babel-loader'),
options: {
presets: [
''@babel/preset-env', ], }, }, exclude: [ getCwdPath('./node_modules') // 因为 node_modules 都是编译过的文件,这里作过滤处理 ] }, { test: /\.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: 1, }, }, { loader: 'postcss-loader', options: { postcssOptions: { plugins: [ [ 'postcss-preset-env', { ident: "postcss" }, ], ], }, } } ], }, { test: /\.(woff(2)?|eot|ttf|otf|svg|)$/, type: 'asset/inline', }, { test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], loader: 'url-loader', options: { limit: 10000, name: 'static/media/[name].[hash:8].[ext]', }, }, ] }, plugins: [ new HtmlWebpackPlugin({ template, filename: 'index.html', }), ], resolve: { extensions: [ '', '.js', '.json', '.sass' ] }, } } 复制代码
上述是一份简化版本的 webpack 5 配置,再添加对应的 commander 命令。
program
.version('0.1.0')
.description('start eslint and fix code')
.command('webpack')
.action((value) => {
buildWebpack()
})
复制代码
如今能够命令行进入测试工程执行 fe-cli webpack
便可获得下述构建产物
下图是使用 CRA 构建出来的产物,跟上图的构建产物对一下,能明显看出使用简化版本的 webpack 5 配置还有不少可优化的地方,那么感兴趣的同窗能够再自行优化一下,做为 demo 已经完成初步的技术预研,达到了预期目标。
此时,若是熟悉构建这块的同窗应该会想到,除了 webpack 的配置项外,构建中绝大部分的依赖都是来自测试工程里面的,那么如何肯定 React 版本或者其余的依赖统一呢?
常规操做仍是经过模板来锁定版本,可是业务同窗依然能够自行调整版本依赖致使不一致,并不能保证依赖一致性。
既然整个构建都由 CLI 接管,只须要考虑将所有的依赖转移到 CLI 所在的项目依赖便可。
Webpack 配置项新增下述两项,指定依赖跟 loader 的加载路径,不从项目所在 node_modules 读取,而是读取 CLI 所在的 node_modules。
resolveLoader: {
modules: [getDirPath('../../node_modules')]
}, // 修改 loader 依赖路径
resolve: {
modules: [getDirPath('../../node_modules')],
}, // 修改正常模块依赖路径
复制代码
同时将 babel 的 presets 模块路径修改成绝对路径,指向 CLI 的 node_modules(presets 会默认从启动路劲读取依赖)。
{
test: /\.(js|jsx)$/,
use: {
loader: getRePath('babel-loader'),
options: {
presets: [
getRePath('@babel/preset-env'),
[
getRePath("@babel/preset-react"),
{
"runtime": "automatic"
}
],
],
},
},
exclude: [
[getDirPath('../../node_modules')]
]
}
复制代码
完成依赖修改以后,一块儿测试一下效果,先将测试工程的依赖 node_modules
所有删除
再执行 fe-cli webpack
,使用 CLI 依赖来构建此项目。
能够看出,已经能够在项目不安装任何依赖的状况,使用 CLI 也能够正常构建项目了。
那么目前全部项目的依赖、构建已经所有由 CLI 接管,能够统一管理依赖与构建流程,若是须要升级依赖的话可使用 CLI 统一进行升级,同时业务开发同窗也没法对版本依赖进行改动。
这个解决方案要根据自身的实际需求来实施,全部的依赖都来源于 CLI 工具的话,版本升级影响会很是大也会很是被动,要作好兼容措施。好比哪些依赖能够取自项目,哪些依赖须要强制通用,作好取舍。
若是遇到最开始提到那些问题的同窗们,应该会常常陷入到业务中没法自拔,并且写这种基础项目,是真的很花时间也很枯燥。容易对工做厌烦,对 coding 感受无趣。
这是很正常的,绝大多数人都有这段经历与相似的想法,但仍是但愿你能去多想一想,在枯燥、无味、重复的工做中去发现痛点、机会。只有接近业务、熟悉业务,才有机会去优化、革新、创造。
全部的基建都是要依托业务才能发挥最大的做用。
天天抽个半小时思考一下今天的工做还能在哪些方面有所提升,提升效率的不只仅是你的代码也能够是其余的工具或者是引入新的流程。
同时也不要仅仅限制在思考阶段,有想法就争取落地,再多抽半小时进行 coding 或者找工具什么的,但凡可以提升个几分钟的效率,即便是个小工具、多几行代码、换个流程这种也值得去尝试一下。
等你把这些零碎的小东西、想法一点点所有积累起来,到最后整合到一个体系中去,那么此时你会发现已经能够站在更高一层的台阶去思考、规划下一阶段须要作的事情,而这其中全部的经历都是你将来成长的基石。
一直相信一句话:努力不会被辜负,付出终将有回报。此时敲下去的每一行代码在将来都将是你登高的一步步台阶。
本章介绍的 CLI 工具还不够完善,做为工程化的一个起点,后续还须要对 CLI 作更多的功能迭代。
对工程化感兴趣的同窗能够关注一下《前端工程化》专栏,一块儿打造一个适合团队的 DevOps 体系。