前言
NodeJs的出現,讓前端工程化的理念不斷深入。先是帶來了Gulp、Webpack等強大的構建工具,隨后又出現了vue-cli
和create-react-app
等完善的腳手架,提供了完整的項目架構,讓我們可以更多的關注業務,而不必在項目基礎設施上花費大量時間。
但是,這些現成的腳手架未必就能滿足我們的業務需求,也未必是最佳實踐,相信每個大公司都有定制開發的的腳手架,今天我們來讀一下京東 NutUI 組件庫中的內置腳手架 NutUI-CLI
NutUI CLI 簡介
NutUI-CLI 是一個 Vue 組件庫構建工具,通過它可以搭建一套 Vue 組件庫
功能
- dev 本地調試運行官網和Demo示例
- add 快速創建符合
NutUI
的標准組件 - build 構建組件庫,生成可用於生產環境的組件代碼
- build-site 構建組件庫官網+Demo示例網站
- ...
為了讓大家快速的了解內部邏輯,我梳理了一個腦圖供大家參考
NutUI-CLI 源碼地址 github.com/jdf2e/nutui…
具體程序流程順序可分為 入口命令腳本接受/分發器
> 命令接收器
> 編譯邏輯處理
> webpack配置

1. 入口命令腳本接受/分發器
CLI 在 NutUI 中是如何被調用起來的
相信大家對下面 @vue/cli 腳手架的命令並不陌生
$ npm run serve
復制代碼
{
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
}
}
復制代碼
NutUI 中使用方式也是如此
$ npm run dev
復制代碼
"scripts": {
"dev": "nutui-cli dev",
"build": "nutui-cli build",
"build:site": "nutui-cli build-site",
"add": "nutui-cli add"
},
復制代碼
我們可以看到 vue/cli執行的實際命令是
$ vue-cli-service serve
復制代碼
NutUI執行的實際命令是
$ nutui-cli dev
復制代碼
此時我們思考一下,vue-cli-service
和nutui-cli
這個命令是如何被我們的項目感知的呢
那么此時,我要在這里啰嗦兩句
Node.js 之 process.argv
process.argv 屬性返回一個數組,其中包含當啟動 Node.js 進程時傳入的命令行參數。 第一個元素是 process.execPath。 如果需要訪問 argv[0] 的原始值,參閱 process.argv0。 第二個元素將是正在執行的 JavaScript 文件的路徑。
例如,假設 process-args.js 的腳本如下:
// 打印 process.argv。
process.argv.forEach((val, index) => {
console.log(`${index}: ${val}`);
});
復制代碼
啟動 Node.js 進程:
$ node process-args.js one two=three four
復制代碼
輸出如下:
0: /usr/local/bin/node
1: /Users/mjr/work/node/process-args.js
2: one
3: two=three
4: four
復制代碼
查看CLI 目錄 中 package.json

cli/package.json
...
"bin": {
"nutui-cli": "./dist_cli/bin/index.js"
},
"scripts": {
"dev": "tsc --watch --incremental"
}
...
復制代碼
package.json
中的 bin屬性 用來指定各個內部命令對應的可執行文件的位置,上述配置我們可以看到 nutui-cli
命令執行的對應腳本為 ./dist_cli/bin/index.js
那么我們此時去查看一下github 中的對應位置
發現並沒有這個目錄,這是怎么一回事呢,思考一下 🤔, 我們發現CLI內部 src 目錄下 並沒有js文件,全部都是 TypeScript 文件。
😯~~ 原來是這樣, 我們可以發現 package.json
scripts 中的 dev : tsc --watch --incremental
有這個命令。 既然源碼中有這個 dev 命令,那我就先運行一下
// 進入cli 源碼目錄
$ cd lib/plugin/cli/
// 安裝依賴...
$ yarn
// 安裝完成后執行dev命令
$ npm run dev
復制代碼

此時再看一下項目中文件夾,發現dist_cli有了

那么 🤔這個 tsc
又是什么呢, 經過搜索后, tsc 其實是TypeScript的編譯命令,將會把ts文件轉換為js 這又得說起 TypeScript
之 tsconfig.json
,先打開tsconfig.json
看看
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"sourceMap": true,
"outDir": "./dist_cli",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": [
"node_modules"
]
}
復制代碼
我們可以看到關鍵點 outDir
代表輸出路徑 、 include
代表編譯路徑。 文檔>tsconfig.json
大家如果還不懂TS的話,可要抓緊補一下了,在Vue3.0中的源碼基本都是TS ,可見TS的重要性
tips: 這里啰嗦兩句,我們在讀源碼的過程中就是要這樣,刨根問底,打破砂鍋搜(百度、谷歌)到底 ,不懂就去搜索搞明白。
ok,上面我們已經了解了TS的基本語法,接下來真正的讀一下
入口命令腳本接受/分發器 bin/index.ts
#!/usr/bin/env node
import { setNodeEnv } from '../util';
process.argv[2] === 'dev' ? setNodeEnv('development') : setNodeEnv('production');
import program from 'commander';
import { dev } from '../commands/dev';
import { build } from '../commands/build';
import { buildSite } from '../commands/build-site';
...
const config = require(ROOT_CLI_PATH('package.json'));
program.version(`@nutui/cli ${config.version}`, '-v', '--version')
program.command('dev')
.description('本地調試運行官網和Demo示例')
.action(dev)
program.command('build')
.description('構建完整版nutui和各個組件可發布到npm的靜態資源包')
.action(build)
program.command('build-site')
.description('構建官網和Demo示例,進行官網發布')
.action(buildSite)
...
program.parse(process.argv);
復制代碼
我們進行逐一解析
#!/usr/bin/env node
復制代碼
該命令必須放在第一行, 否者不會生效
在寫npm包的時候需要在腳本的第一行寫(必須),用於指明該腳本文件要使用node來執行。
/usr/bin/env 用來告訴用戶到path目錄下去尋找node,#!/usr/bin/env node 可以讓系統動態的去查找node,已解決不同機器不同用戶設置不一致問題。
node 命令的工具 commander
我們可以看到nutui中第4行引入了第三方庫: commander
import program from 'commander';
復制代碼
我們從整體代碼上來看不難理解,commander
去監聽用戶輸入的指令 dev
build
...等命令,然后去觸發action
指向的對應方法 ,到這里,我相信大家對 命令分發器這個模塊 已經了如指掌了,那么代碼中的
import { dev } from '../commands/dev';
import { build } from '../commands/build';
復制代碼
對應的 dev build
等方法,我們可以看到都是從 commands 命令接收器
模塊中引入,那么接下來,我們接着介紹 命令接收器
2. 命令接收器
tips:我們可通過
ctrl
|command
鍵 + 鼠標左鍵點擊 快速跳轉到對應方法

這里只解析三個模塊命令,其它的命令大致相同,感興趣大家也可以去讀一下
本地調試 dev.ts
import { compileSite } from '../compiler/site';
export async function dev() {
await compileSite();
}
復制代碼
網站構建 build-site.ts
import { emptyDir } from 'fs-extra';
import { compileSite } from '../compiler/site';
import { DIST_DIR } from "../util/dic";
export async function buildSite() {
await emptyDir(DIST_DIR);
await compileSite(true);
process.exit()
}
復制代碼
import { emptyDir } from 'fs-extra';
復制代碼
fs-extra
文件操作包,不過多介紹,大家可以去看文檔
我們主要看一下 dev.ts
和 buildSite.ts
中同時引用了 compileSite
關鍵方法,只是傳入的參數不同,通過功能介紹我們得知 dev
是為了本地調試官網+demo示例,而build-site
是為了構建官網+demo示例,那么這里可以大致猜到compileSite
的參數 分別是為了區分 dev和build構建。稍后我們去看一下compileSite
邏輯。
組件庫構建 build.ts
import { emptyDir } from 'fs-extra';
import { compilePackage } from '../compiler/package';
import { DIST_DIR } from "../util/dic";
import logger from '../util/logger';
import { compilePackageDisperse } from '../compiler/package.disperse';
export async function build() {
try {
await emptyDir(DIST_DIR);
await compilePackage(false);
logger.success(`build compilePackage false package success!`);
await compilePackage(true);
logger.success(`build compilePackage true package success!`);
await compilePackageDisperse();
logger.success(`build compilePackageDisperse package success!`);
process.exit();
} catch (error) {
logger.error(error)
}
}
復制代碼
build
這邊就有意思了,分別是 compilePackage
和 compilePackageDisperse
這兩個重要方法 而compilePackage
和 compileSite
使用方法類似,分別傳入 true
false
, compilePackageDisperse
則是直接調用。
compileSite
、compilePackage
、compilePackageDisperse
這三個重要方法都是從compiler
模塊拿到的,那么此時進入下一小節逐一解讀。
3. 編譯邏輯處理
網站編譯 site.ts > compileSite
import { devConfig } from '../webpack/dev.config';
import { prodConfig } from '../webpack/prod.config';
import { compileWebPack } from './webpack';
import logger from '../util/logger';
export async function compileSite(prod: boolean = false) {
try {
prod ? await compileWebPack(prodConfig) : compileWebPack(devConfig);
prod && logger.success('build site success!');
} catch (error) {
logger.error(error);
}
}
復制代碼
組件庫編譯 package.ts > compilePackage
import { packageConfig } from '../webpack/package.config';
import { compileWebPack } from './webpack';
export function compilePackage(isMinimize: boolean) {
return compileWebPack(packageConfig(isMinimize))
}
復制代碼
按需加載組件庫編譯 package.disperse.ts > compilePackageDisperse
import { compileWebPack } from './webpack';
import { packageDisperseConfig } from '../webpack/package.disperse.config';
export function compilePackageDisperse() {
return compileWebPack(packageDisperseConfig())
}
復制代碼
我們可以看到 上面 devConfig
、prodConfig
、packageConfig
這三個config 都是從webpack文件夾中依次按需提取,其實關鍵點在於 webpack.ts > compileWebPack 這個方法 它才是重中之重,負責核心編譯
核心構建 webpack.ts
import Webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import logger from "../util/logger";
function devServer(config: Webpack.Configuration) {
const compiler = Webpack(config);
const devServerOptions = {
...
};
const server = new WebpackDevServer(compiler, devServerOptions);
server.listen(8001, '0.0.0.0', (err: Error) => {
if (err) logger.error(err);
});
}
function build(config: Webpack.Configuration) {
return new Promise((resolve, reject) => {
Webpack(config, (err: any, stats) => {
if (err || stats.hasErrors()) {
// 在這里處理錯誤
...
reject(err || stats.toJson())
} else {
// 處理完成
resolve();
}
});
});
}
export function compileWebPack(config: Webpack.Configuration) {
switch (config.mode) {
case "development":
devServer(config);
break;
case "production":
return build(config);
}
}
復制代碼
我們可以看到webpack.ts 對外暴露 export function compileWebPack
入參方法強制約束為 Webpack.Configuration
類型 這個地方充分體現了ts強約束給我帶來的好處,其方法內部通過 config.mode
屬性來區分 進行 devServer
還是 build
構建。 看到這里,不知道大家是否注意到有在文件的頂部2行代碼
import Webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
復制代碼
這個地方重點說一下,在傳統的
vue/cli2
腳手架調用時,是通過下面 WebPack CLI 命令 在項目 package.json 進行調用
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"build": "node build/build.js"
}
復制代碼
而在 NutUI-CLI 則是通過 WebPack Node API WebpackDevServer
、Webpack
來進行調用,那么這樣調用,有什么好處呢。
引用webpack官方文檔:webpack 提供了 Node.js API,可以在 Node.js 運行時下直接使用。 當你需要自定義構建或開發流程時,Node.js API 非常有用,因為此時所有的報告和錯誤處理都必須自行實現,webpack 僅僅負責編譯的部分。所以 stats 配置選項不會在 webpack() 調用中生效。
那么我們現在知道了,compileWebPack 只負責編譯 ,文件全部從 webpack文件夾中提取,我們接下來進入下章 WebPack配置
4. WebPack配置
base.config.ts、dev.config.ts、prod.config.ts這三個配置文件主要用於 dev調試 和打包網站,平時大家項目中都有用到, 我就不過多介紹了,大家可以通過webpack官方文檔去了解,我們主要講一下package.config.ts
和package.disperse.config.ts
這兩個配置文件,主要用來構建組件庫。
package.config.ts
import Webpack from 'webpack';
import merge from 'webpack-merge';
import { baseConfig } from './base.config';
...
export function packageConfig(isMinimize: boolean) {
const _packageConfig: Webpack.Configuration = {
mode: 'production',
devtool: 'source-map',
entry: {
nutui: ROOT_PACKAGE_PATH('src/nutui.js'),
},
output: {
path: ROOT_PACKAGE_PATH('dist/'),
filename: isMinimize ? '[name].min.js' : '[name].js',
library: '[name]',
libraryTarget: 'umd',
umdNamedDefine: true,
// https://stackoverflow.com/questions/49111086/webpack-4-universal-library-target
globalObject: `(typeof self !== 'undefined' ? self : this)`
},
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
}
},
...
};
isMinimize && _packageConfig.plugins?.push(new OptimizeCSSAssetsPlugin())
return merge(baseConfig, _packageConfig);
}
復制代碼
entry: { nutui: ROOT_PACKAGE_PATH('src/nutui.js'), }
src/nutui.js 將所有組件封裝統一打包output.libraryTarget: 'umd'
- 將你的 library 暴露為所有的模塊定義下都可運行的方式。它將在 CommonJS, AMD 環境下運行,或將模塊導出到 global 下的變量。了解更多請查看 UMD 倉庫。- 當使用了 libraryTarget: "umd",設置:
umdNamedDefine: true
會對 UMD 的構建過程中的 AMD 模塊進行命名。否則就使用匿名的 define。 output.externals: { vue: { root: 'Vue', commonjs: 'vue', commonjs2: 'vue', amd: 'vue' } }
- root:可以通過一個全局變量訪問 library(例如,通過 script 標簽)。
- commonjs:可以將 library 作為一個 CommonJS 模塊訪問。
- commonjs2:和上面的類似,但導出的是 module.exports.default.
- amd:類似於 commonjs,但使用 AMD 模塊系統。
output.globalObject
當以庫為目標時,特別是當libraryTarget為“umd”時,此選項指示將使用哪個全局對象來裝載庫。要使UMD build在瀏覽器和Node.js上都可用,請將output.globalObject選項設置為“this”。
使用此配置文件打包 可以生成我們全局引入的js文件

ok ,全局引入組件的原理我們搞清楚了,接下來分析下 按需加載每個默認的配置文件package.disperse.config
。
package.disperse.config.ts
import Webpack from 'webpack';
import merge from 'webpack-merge';
import { ROOT_PACKAGE_PATH } from '../util/dic';
import { baseConfig } from './base.config';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin';
const confs = require(ROOT_PACKAGE_PATH('src/config.json'));
export function packageDisperseConfig() {
const entry: any = {};
confs.packages.map((item: any) => {
const cptName: string = item.name.toLowerCase();
entry[cptName] = ROOT_PACKAGE_PATH(`src/packages/${cptName}/index.js`);
});
const _packageDisperseConfig: Webpack.Configuration = {
mode: 'production',
devtool: 'source-map',
entry,
...
plugins: [
...
new CopyWebpackPlugin([
{
from: ROOT_PACKAGE_PATH('src/'),
to: ROOT_PACKAGE_PATH('dist/'),
ignore: ['demo.vue', 'doc.md', 'config.json', 'nutui.js', '*.spec.js']
}
]),
new CopyWebpackPlugin([
{
from: ROOT_PACKAGE_PATH('types/'),
to: ROOT_PACKAGE_PATH('dist/types/')
}
]),
]
};
return merge(baseConfig, _packageDisperseConfig);
}
復制代碼
核心代碼,將每一個組件作為一個入口,構成多入口
const confs = require(ROOT_PACKAGE_PATH('src/config.json'));
const entry: any = {};
confs.packages.map((item: any) => {
const cptName: string = item.name.toLowerCase();
entry[cptName] = ROOT_PACKAGE_PATH(`src/packages/${cptName}/index.js`);
});
復制代碼
我們可以看到 confs.packages中
"packages": [
{
"name": "Cell",
"version": "1.0.0",
"sort": "4",
"chnName": "列表項",
"type": "component",
"showDemo": true,
"desc": "列表項,可組合成列表",
"author": "Frans"
},
{
"name": "Dialog",
"version": "1.0.0",
"sort": "2",
"chnName": "對話框",
"type": "method",
"showDemo": true,
"desc": "模態彈窗,支持按鈕交互,支持圖片彈窗。",
"star": 5,
"author": "Frans"
},
...
]
復制代碼
通過過多入口可以構建出 每個組件的js和css

那么我們實際在dev開發模式時還需引入vue的源代碼,源代碼又是如何被打包呢,見plugin
中的CopyWebpackPlugin
插件 源碼就比較簡單了,直接拷貝過來,此時再進行打包測試
plugins: [
new MiniCssExtractPlugin({
filename: '[name]/[name].css'
}),
new OptimizeCSSAssetsPlugin(),
new CopyWebpackPlugin([
{
from: ROOT_PACKAGE_PATH('src/'),
to: ROOT_PACKAGE_PATH('dist/'),
ignore: ['demo.vue', 'doc.md', 'config.json', 'nutui.js', '*.spec.js']
}
]),
new CopyWebpackPlugin([
{
from: ROOT_PACKAGE_PATH('types/'),
to: ROOT_PACKAGE_PATH('dist/types/')
}
]),
]
復制代碼
打包成功,完整的 npm 靜態資源包



總結
文章的主要目的是鼓勵大家更加主動閱讀和學習源碼,幫助大家學更多 vue 和webpack的 相關知識,使我們成為更優秀的開發者。定期投資幾個小時來閱讀源碼,短期無效,長遠看來,是受益無窮的事情。通過閱讀源碼,你將從更深層次了解你平時所用框架的工作原理,反之又促進你更好的使用、擴展它。從而幫助你縮短從需求到編碼的完成時間。
最后,附上 NUTUI 組件庫官方網址 nutui.jd.com
