preface

With NodeJs, the concept of front-end engineering continues to deepen. First came powerful build tools such as Gulp and Webpack, followed by sophisticated scaffolding such as VUe-CLI and creation-React-app, which provided a complete project architecture that allowed us to focus more on the business without spending a lot of time on the project infrastructure.

However, these off-the-shelf scaffoldings may not meet our business needs or best practices. We believe that every large company has custom developed scaffoldings. Today we will read the built-in scaffolding nutui-CLI in JD NutUI component library

Introduction of NutUI CLI

NutUI-CLI is a Vue component library building tool, through which you can build a set of Vue component library

function

  • Dev native debugging runs the official website and Demo examples
  • Add Quickly create matchesNutUIStandard components of
  • Build builds a component library that generates component code that can be used in a production environment
  • Build -site build component library official website +Demo site
  • .

In order to give you a quick understanding of the internal logic, I combed a brain map for your reference

NutUI-CLI source address github.com/jdf2e/nutui…

The specific program flow sequence can be divided into entry command script accept/distributor > command receiver > compile logic processing > Webpack configuration


1. Entry command script acceptor/distributor

How is CLI invoked in NutUI

You are familiar with the following @vue/ CLI scaffolding commands

$ npm run serve

Copy the code
{

"scripts": {

"serve": "vue-cli-service serve".

"build": "vue-cli-service build"

}

}

Copy the code

The same is true in NutUI

$ npm run dev

Copy the code
"scripts": {

"dev": "nutui-cli dev".

"build": "nutui-cli build".

"build:site": "nutui-cli build-site".

"add": "nutui-cli add"

},

Copy the code

We can see that the actual command executed by vue/ CLI is

$ vue-cli-service serve

Copy the code

The actual command NutUI executes is

$ nutui-cli dev

Copy the code

Now let’s think about how vue-cli-service and nutui-cli are perceived by our project

So at this point, I’m going to say a few words here

The process of the Node. Js argv

The process.argv property returns an array containing the command line arguments passed in when the Node.js process is started. The first element is process.execPath. If you need access to the original value of argv[0], see process.argv0. The second element will be the path to the JavaScript file being executed.

For example, suppose the script for process-args.js is as follows:

// Prints process.argv.

process.argv.forEach((val, index) = > {

console.log(`${index}: ${val}`);

});

Copy the code

To start the Node.js process:

$ node process-args.js one two=three four

Copy the code

The output is as follows:

0: /usr/local/bin/node

1: /Users/mjr/work/node/process-args.js

2: one

3: two=three

4: four

Copy the code

View package.json in the CLI directory

cli/package.json

.

"bin": {

"nutui-cli": "./dist_cli/bin/index.js"

},

"scripts": {

"dev": "tsc --watch --incremental"

}

.

Copy the code

The bin property in package.json is used to specify the location of the executable for each internal command, The nutui-cli command is executed in the./dist_cli/bin/index.js script

There are no js files in the SRC directory inside the CLI. They are all TypeScript files.

😯~~ incremental for package.json scripts dev: TSC –watch –incremental Since the source code has the dev command, I will run it first

// Go to the CLI source directory

$ cd lib/plugin/cli/

// Install dependency...

$ yarn

// After the installation, run the dev command

$ npm run dev

Copy the code

If you look at the folder in your project, dist_cli is present

TSC is a TypeScript compiler that converts ts files to JS. Tsconfig. json is TypeScript’s 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"

]

}

Copy the code

We can see that the key points outDir represents the output path and include represents the compile path. Document > tsconfig. Json

If you do not understand TS, you can pay close attention to the repair, in THE Vue3.0 source code is basic TS, visible TS importance

Tips: here wordy two sentences, we read the source code in the process is to such, get to the bottom of the matter, break the casserole search (Baidu, Google) in the end, do not understand to search to figure out.

Ok, now that we know the basic syntax of TS, let’s actually read it

Entry command script accepts/dispenser 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('Local debug run official website and Demo sample')

.action(dev)

program.command('build')

.description('Build a full version of Nutui and a static bundle of components that can be published to NPM')

.action(build)

program.command('build-site')

.description('Build the official website and Demo and launch the official website')

.action(buildSite)

.

program.parse(process.argv);

Copy the code

Let’s go through them one by one

#! /usr/bin/env node

Copy the code

The command must be placed in the first line. Otherwise, it will not take effect

When writing an NPM package, write (must) the first line of the script to indicate that the script file is to be executed using Node.

/usr/bin/env is used to tell the user to look for node in the path directory, #! The /usr/bin/env node allows the system to dynamically look up nodes, which eliminates the problem of different user Settings on different machines.

The node command tool commander

We can see that line 4 of Nutui introduces a third-party library: Commander

import program from 'commander';

Copy the code

Dev build, dev build, dev build, dev build And so on command, and then to trigger the corresponding method that action points to, here, I believe everyone to command distributor this module has been like the palm of your hand, so in the code

import { dev } from '.. /commands/dev';

import { build } from '.. /commands/build';

Copy the code

The corresponding dev build and other methods are imported from the Commands command sink module, so we will continue to introduce the command sink

2. Command receiver

Tips: we can use CTRL | command key + left mouse button click on the quick jump to the corresponding method


Only three module commands are parsed here. The rest of the commands are roughly the same. You can read them if you are interested

Debug dev.ts locally

import { compileSite } from '.. /compiler/site';

export async function dev() {

await compileSite();

}

Copy the code

Site build 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()

}

Copy the code
import { emptyDir } from 'fs-extra';

Copy the code

Fs-extra file operation package, but more introduction, you can go to see the documentation

Dev. Ts and buildsite. ts refer to the key compileSite methods, but pass them different parameters. Build-site is used to build official website +demo samples, so you can guess the parameter to compileSite is used to distinguish dev from build. Let’s look at the compileSite logic later.

The component library builds 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)

}

}

Copy the code

Now, build is interesting. These are important methods called compilePackage and compilePackageDisperse and you can use them in the same way, you pass in true false, Fetch ackageDisperse is called directly.

The three important methods compileSite, compilePackage and compilePackageDisperse are all obtained from the Compiler module, so let’s go to the next section to interpret them one by one.

3. Compilation logic processing

Site > 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);

}

}

Copy the code

Component libraries compile package.ts > compilePackage

import { packageConfig } from '.. /webpack/package.config';

import { compileWebPack } from './webpack';

export function compilePackage(isMinimize: boolean) {

return compileWebPack(packageConfig(isMinimize))

}

Copy the code

Load the component library to compile package.disperse. Ts > compilePackageDisperse

import { compileWebPack } from './webpack';

import { packageDisperseConfig } from '.. /webpack/package.disperse.config';

export function compilePackageDisperse() {

return compileWebPack(packageDisperseConfig())

}

Copy the code

You can see that devConfig, prodConfig, and packageConfig are extracted from the WebPack folder as needed. The key to this is the webpack.ts > compileWebPack method which is the key to core compilation

The core builds 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()) {

// Handle the error here

.

reject(err || stats.toJson())

} else {

// The processing is complete

resolve();

}

});

});

}

export function compileWebPack(config: Webpack.Configuration) {

switch (config.mode) {

case "development":

devServer(config);

break;

case "production":

return build(config);

}

}

Copy the code

Webpack. ts exposes export function compileWebPack to webpack. Configuration. The config.mode attribute internally distinguishes devServer from build. If you look at this, I don’t know if you noticed that there are two lines of code at the top of the file

import Webpack from 'webpack';

import WebpackDevServer from 'webpack-dev-server';

Copy the code

It is important to note that in a traditional vue/ CLI2 scaffold invocation, the following WebPack CLI command is invoked in the project package.json

 "scripts": {

"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js".

"start": "npm run dev".

"build": "node build/build.js"

}

Copy the code

Nutui-cli is called via WebPack Node API WebpackDevServer, WebPack.

Webpack provides the Node.js API, which can be used directly when Node.js is running. The Node.js API is very useful when you need to customize your build or development process, because all the reporting and error handling must be done by yourself, and WebPack only takes care of the compilation part. So the STATS configuration option does not take effect in the Webpack () call.

So now we know that compileWebPack is only responsible for compilation, files are all extracted from webpack folder, let’s move on to webPack configuration

4. WebPack configuration

Ts, dev. Config. ts, prod. Config. ts these three configuration files are mainly used for dev debugging and packaging website, usually everyone in the project useful, I will not introduce more, you can learn through the official documentation of webpack. We mainly talk about the package. The config. Ts and package. Disperse. Config. The ts the two configuration files, and is mainly used to construct the component library.

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);

}

Copy the code
  • entry: { nutui: ROOT_PACKAGE_PATH('src/nutui.js'), } src/nutui.jsEncapsulate all components in a unified package
  • output.libraryTarget: 'umd'– Expose your library in a way that all module definitions work. It will run in CommonJS, AMD environments, or export modules to variables under Global. Find out moreUMDThe warehouse.
  • When using libraryTarget: “umd”, set:umdNamedDefine: trueAMD modules in the UMD build process are named. Otherwise use anonymous define.
  • output.externals: { vue: { root: 'Vue', commonjs: 'vue', commonjs2: 'vue', amd: 'vue' } }
    • Root: The Library can be accessed through a global variable (for example, through the script tag).
    • Commonjs: Library can be accessed as a CommonJS module.
    • Commonjs2: same as above, but exports module.exports.default.
    • Amd: Similar to CommonJS, but using amd module system.
  • output.globalObjectWhen a library is targeted, especially if the libraryTarget is “UMD,” this option indicates which global object will be used to load the library. To make UMD builds available in both browsers and Node.js, set the output.GlobalObject option to “this”.

Use this configuration file package to generate our globally imported JS files

Ok, we have figured out the principle of importing components globally. Next, we will analyze the loading of each default configuration file package.disperse. Config as required.

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);

}

Copy the code

The core code, each component as an entry, constitutes a multiple entry

 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`);

});

Copy the code

We can see that in Confs.Packages

"packages": [

{

"name": "Cell".

"version": "1.0.0".

"sort": "4".

"chnName": "List item".

"type": "component".

"showDemo": true.

"desc": "List items that can be combined into lists.".

"author": "Frans"

},

{

"name": "Dialog".

"version": "1.0.0".

"sort": "2".

"chnName": "Dialog box".

"type": "method".

"showDemo": true.

"desc": "Modal popover, support button interaction, support image popover.".

"star": 5.

"author": "Frans"

},

.

]

Copy the code

There are multiple entrances to build js and CSS for each component

So we actually need to introduce vue source code in dev development mode, source code is how to be packaged, see the plugin in CopyWebpackPlugin source code is relatively simple, directly copy over, at this time to package test

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/')

}

]),

]

Copy the code

Package successfully, complete NPM static resource pack



conclusion

The main purpose of this article is to encourage you to read and learn more about the source code, to help you learn more about Vue and Webpack, and to make us better developers. Investing a few hours on a regular basis to read source code won’t work in the short term, but will pay off in the long run. By reading the source code, you will gain a deeper understanding of how your framework works, which in turn will encourage you to use and extend it better. This will help you shorten the completion time from requirements to code.

Finally, attach the official website of NUTUI component library, nutui.jd.com