⚠️ 本文爲掘金社區首發簽約文章,未獲受權禁止轉載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 體系。