preface
Gradually, we have become the most hated person (clickbait).
However, compared with the form, the content is obviously more important. I hope readers can fully experience the psychological process of developing a scaffold from zero to one through this article.
Research & Discussion
Although the project is not as important as it should be, there should be a project flow. I first recruited two members through Amway. Briefly communicated with everyone in the meeting:
1. Research on mainstream schemes in the market
First of all, when it comes to React scaffolding, create-React-app, which dominates the React community, is inevitable. However, it was unexpectedly ruled out at the first time. This is mainly due to its inflexible configuration, the lack of encapsulation of WebPack, which makes it difficult to handle complex projects, and the functionality it provides is generic but rudimentary. I’m sure someone at this point would say, well, can you eject it and expose the source code? But in this case, I use it has no meaning, and based on its source code secondary development, the cost is certainly not low.
Then, we turned to VUe-CLI, which unexpectedly won unanimous praise from everyone. I think the best part of vue-CLI is that it achieves a good balance in the two indicators of versatility and flexibility, which is highly consistent with the philosophy of vue.js framework. Therefore, combining with VUE to develop the project, There will be a sense of flow, and the unanimous love of its CLI (interactive command line tool), is the following east, used by people must be impressed.
React does not support react?
Our goal, then, is to develop a React scaffolding that works like vue-CLI. Previously, @vue/ CLI was only used as a layer, so you need to understand its implementation first. Just as the saying goes, only when you know yourself, can you realize it.
2. @vue/ CLI source code analysis
I clone version 4.5.14, 5.0 was still in beta when I started, so I skipped it.
The overall code is maintained using Lerna + Yarn-workspace, and packages related to formal vUE functions are contained under the @vue NPM domain (some also call it the NPM organization).
As shown in the figure above, the complete @vue/ CLI functionality consists of three main parts:
@vue/cli
Collect command line parameters@vue/cli-service
This package is the engine and core that launches vUE and hosts the WebPack configuration.@vue/plugin-xxx
Vue-cli plug-ins, each plug-in corresponds to a function, which corresponds to the above user-defined feature options.
Cli and core function package separation is a mainstream way to unpack webpack-CLI & Webpack,@babel/ CLI & @babel/core are all implemented in this way, belonging to the layered architecture of software, cli (upper layer) needs to rely on core services (lower layer), However, core services (lower level) can run independently without relying on CLI (upper level).
The first step in reading the source code is to start with package.json. These global packages rely on the bin field to implement global commands.
"bin": {
"vue": "bin/vue.js"
},
Copy the code
Follow the lead. Open itbin/vue.js
Here and hereCommander
This command line parsing tool registers some global commands, which we will focus on herecreate
This command is OK. In the execution of globalvue create xxx
Command is used to run this file.And as you can see at the end,vue create xxx
The command is executed when triggered../lib/create
This file will put the project namename
Send it in. Keep tracking it../lib/create.js
.
First, spell out the target path targetDir for the project to be created. In most cases, this is the directory where the vue create command was typed + the project name that was passed in.
This is followed by the code to process an existing project of the same name on disk, followed by the new Creator class, which officially starts the main process of creating the project.
Finding this Creator class, you can find that it inherits EventEmiter, the event processing class, in order to realize its whole plug-in mechanism, which is similar to the process of writing webpack plug-in, which needs to subscribe to some hooks distributed by complation to realize the event mode based on publishing and subscription. And we’ll talk about that in detail.
At this point you need to open the breakpoint debugging, it will run the process to see more clearly.
Is the interaction option in the red box on the left ever similar? Yes, its corresponding interface has appeared above
The Creator class constuctor initializes property variables, and the core is the create instance method.
If promptAndResolvePreset is invoked, the preset options screen is displayed. Select a default option and proceed
On the left you can see presets, the selected default data structure, which includes Babel, ESLint, and more.
Switch to a preset and choose manual? The following figure shows the same data structure.
The next step is to initialize some initial configuration for each of the plugins in the Presets array
Next, initialize the data needed to generate package.json.
Insert the plug-in you included in the preset into the devDependencies development dependency.
The insertion succeeded, and you can see that the debug stack contains three development dependencies
Next, write package.json to disk.
Run NPM install to install dependencies.
Then I do some more: write NPMRC, YARnRC files, initialize git repository, etc.
The important point is that the next step is to use the newly installed plug-in to generate the project template.
This.resolveplugins (preset. Plugins, PKG) method is key, but before I introduce it, I want to talk a little about the plugin mechanism of the framework and its design and implementation in VUE/CLI.
Plugin mechanism:
It can encapsulate some optional or user-defined functions and insert them as needed. Such design advantage is that it can split the code complexity and function coupling, and greatly increase the expansibility of the framework.
Plug-in, first of all to decide how to plug, this is the plug-in mechanism of the most important point. Framework developers and plug-in developers need to agree on a set of fixed ways to access plug-ins. This convention is embodied in vUE/CLI, as shown in the following figure.
Each plug-in has a generator folder, which must have an index.js file underneath it. You then need to export a function that can call externally injected utility methods in the function body, for example,
api.injectImport()
Insert a module import into the project.api.extendPackage()
Extend the project’s package.jsonapi.render('./template')
Render template files using EJS (conditional compilation)
Back to the this.resolveplugins (preset. Plugins, PKG) method, the apply is actually the function exported by the plugin convention, but it should be noted that the apply method is only saved temporarily and is not called.
The original plugins, after being treated in this way, are converted into new plugins that contain specific plug-in functionality. { id: options } => [{ id, apply, options }]
Next, pass the finished plugins into the Generator class to begin the final and most substantial step: generating the project.
Suffice it to say, a lot of preparation work has been done to collect parameters, sort them out, and introduce plug-ins for the final generation step.
New is followed by the generate instance method, so let’s focus on the implementation of this method
This.initplugins () is called first. The most important thing in this method is to iterate through all the plugins’ default apply methods (the ones that are exported by default in the previous plugin mechanism). The first parameter to apply is the API, which contains capabilities such as render and render ejS templates from the GeneratorAPI class. Note that the second parameter passes in the current this.
Next, let’s look at the implementation of GeneratorAPI. First, let’s exclude the @vue/ CLI-service package, because although this package exists in the plugins array, it is very special. It belongs to the core service. My guess here is just to keep the data structure as simple as possible and filter it out when you use it.
The overall implementation of the GeneratorAPI uses a middleware design in which the _injectFileMiddleware method on the API is stored as a function push and is not executed.
Taking the api.render() template method as an example, it does trigger the api.render() call when calling the apply method exported by the plug-in, but it doesn’t actually compile the template file using EJS. But will they staged to this. The generator. FileMiddlewares this middle temporary array.
So where do api.render methods really work?
Open the this.resolveFiles method that follows, and that’s where the truth lies.
Each middleware function is called serially with a for of call to get the contents of the resulting file to be generated
At this point, this.files saves the full file path to file content mapping, as shown on the left side of the image above.
Next, some general operations. SortPkg can sort the order based on package.json to satisfy some obsession-compulsive feelings.
We can then call writeFileTree to happily write files to disk, based on this.files.
The resulting directory structure
3. Define functions and objectives
The previous detailed analysis of @vue/ CLI can be summarized in three key points
-
- Use the command line interface to select functions
-
cli
δΈCore services
@vue/ CLI-service (Webpack configuration) adopts layered design,Independent contract
-
- Plug-in mechanism, generate different function template files on demand
Such a design is necessary for a scaffold for all VUE developers because it requires a great deal of flexibility, but for a scaffold that only caters to a specific team, it would be too complex and expensive to develop, requiring a lot of work just to break up the various functional plug-ins. Secondly, the most important thing for a team’s project development is to unify and standardize. Many configurations and functions can be built-in by default, such as ESLint, Babel and CSS processors. These are all necessary functions for team project development, so configuration diversity and flexibility are not the primary consideration. Therefore, the third plugin mechanism can be omitted.
Cli and Webpack configuration can be separated, so that the independent version of both can be updated, which is conducive to the continuous maintenance of user projects, but the cost of doing so is to define a set of configuration conventions that are very different from webPack configuration. This is done in @vue/cli-service using vue.config.js as a configuration file. Other major scaffolding such as create-react-app follows this path. In this case, a detailed scaffolding configuration document is also required to configure webPack by configuring scaffolding, which makes the knowledge of webPack configuration useless. ChainWebpack in VUE is a solution, but only those who have used it can appreciate how difficult it is to use. We may only need a transparent Webpack configuration. All the rules and plugins can be modified directly, so we don’t need documentation. We just need the WebPack configuration to do everything.
So, the main lessons we need to learn are interactive command line tools and compilation of EJS templates on demand. At the same time, we refined the functions based on the project characteristics of our team. After the discussion of our partners, we mainly made the following choices:
(1) Mobile terminal or PC terminal?
Antd will be built into PC, pX2REM adaptation will be enabled on mobile, and the flexible. Js script will be inline in HTML.
(2) Generate single-page or multi-page templates?
MPA and SPA requirements are both present in our project scenario, so it makes sense to distinguish the two at the scaffold level.
(3) Install the status management library and react-Router version as required
The function development
Initialize the project
- monorepo
Its advantage over a single repository is that it is conducive to efficient joint adjustment between multiple NPMS and sharing of node_modules disk space. Because lerNA’s dependency promotion is too strict on the dependency version number, the mainstream practice is to use YARN workspace to do dependency management. Lerna does automatic version management and distribution of NPM packages. The reason for using Monorepo is that later projects such as component libraries or Webpack-plugins-can be developed based on this scaffolding to facilitate co-tuning between them.
npm i -g lerna
lerna init
Copy the code
lerna.json
{" packages ": [" packages / *"], "version" : "0.1.6", "npmClient" : "yarn" and "useWorkspaces" : true}Copy the code
Create a cli package
lerna create react-booster-cli
Copy the code
Initialize the project
yarn install
Copy the code
The next step is to implement the functions of CLI. First of all, when analyzing the Vue/CLI source code, it was mentioned that the global command used by the global package is realized through the bin field of package.json.
booster/packages/react-booster-cli/package.json
"bin": {
"booster": "bin/booster.js"
},
Copy the code
Next, create the./bin/booster.js file
#! /usr/bin/env node // command line parser const program = require('commander'); program .version(require(".. /package").version) .usage("<command> [options]"); Program.com mand("create <project-name>").description(" create a new project ").action((projectName)=>{require('.. /lib/create')(projectName) }) program.parse(process.argv);Copy the code
#! The /usr/bin/env node line is important to state that this file needs to be executed using the Node program. Use the commander command line parameter parsing library
yarn workspace react-booster-cli add commander
Copy the code
Now you can test and run the process in the Booster root directory
npx booster
Copy the code
The following screen is successful
NPX XXX will first go to the current node_modules/.bin/ directory to find the XXX file. Obviously, there is. This is why, we did not do much ah, although the declaration of global command, but write cli package, a package did not send, two did not install.
Yarn Install is an add-in to yarn Install. If you want to use yarn Install, yarn Install will automatically help you install and link. The idea is to create a soft link (similar to a file shortcut on Win) in node_modules/. Bin to packages/react-booster-cli/bin/booster.js.
Following the create command, comander’s action passes the project name to the create method in the create file, which is consistent with vue/ CLI
Function implementation
Implement the create method.
There are a lot of tools used to do the CLI, and the purpose of each tool is explained in the comments below. Many of them are actually used to make the command line more beautiful, perhaps for the front end. In the NPM ecosystem, there are a lot of packages to decorate the command line. For example, some command line loading effects and color fonts and progress bar are implemented to increase the user experience of the command line.
lib/create.js
const path = require("path"); const fs = require("fs"); // Check whether the directory exists const exists = fs.existssync; // delete file const rm = require("rimraf").sync; // ask cli input parameter const ask = require("./ask"); Const inquirer = require("inquirer"); // loading const ora = require("ora"); Const chalk = require("chalk"); // Check version const checkVersion = require("./check-version"); const generate = require("./generate"); const { writeFileTree } = require("./util/file"); const runCommand = require("./util/run"); // loading const spinner = ora(); async function create(projectName) { const cwd = process.cwd(); // Current node directory const projectPath = path.resolve(CWD, projectName); If (exists(projectPath)) {const answers = await inquirer. Prompt ([{type: "confirm", message: "Target directory exists. Do you want to replace it?", name: "ok", }, ]); if (answers.ok) { console.log(chalk.yellow("Deleting old project ..." )); rm(projectPath); await create(projectName); }} else {// Collect user input options const answers = await ask(); spinner.start("check version"); // checkVersion await checkVersion(); spinner.succeed(); Console. log(' β¨ Creating project in ${chalk. Yellow (projectPath)}. '); // console.log(answers); // update package.json const PKG = require(".. /template/package.json"); Json const appConfig = {}; // Generate project config file, app.config.json const appConfig = {}; const { platform, isMPA, stateLibrary,reactRouterVersion } = answers; If (platform === "mobile") {pkg.devDependencies["postcss-pxtorem"] = "^6.0.0"; PKG. Dependencies [" lib - flexible "] = "^ 0.3.2"; } else if (platform === "pc") { pkg.dependencies["antd"] = "latest"; } pkg.dependencies[stateLibrary] = "latest"; If (reactRouterVersion === "v5") {pkg.devDependencies["react-router"] = "5.1.2"; } else if (reactRouterVersion === "v6") { pkg.dependencies["react-router"] = "^6.x"; } appConfig.platform = platform; spinner.start("rendering template"); const filesTreeObj = await generate(answers,projectPath); spinner.succeed(); Spinner. Start (" π invoking generators..." ); await writeFileTree(projectPath, { ... filesTreeObj, "package.json": JSON.stringify(pkg, null, 2), "app.config.json": JSON.stringify(appConfig, null, 2), }); spinner.succeed(); The console. The log (` π Initializing the git repository... `) await runCommand('git init') console.log(); Console. log(' π Successfully created project ${chalk. Yellow (projectName)}. '); Console. log(' π Get started with the following commands:\n\n '+ talk.cyan (' ${talk.gray ("$")} CD ${projectName}\n') + chalk.cyan(` ${chalk.gray("$")} npm install or yarn\n`) + chalk.cyan(` ${chalk.gray("$")} npm run dev`) ); console.log(); } } module.exports = (... args) => { return create(... args).catch((err) => { spinner.fail("create error"); console.error(chalk.red.dim("Error: " + err)); process.exit(1); }); };Copy the code
Test version
lib/check-version.js
const request = require('request') const semver = require('semver') const chalk = require('chalk') const packageConfig = require('.. /package.json') module.exports = function checkVersion() { return new Promise((resolve,reject)=>{ if (! semver.satisfies(process.version, packageConfig.engines.node)) { return console.log(chalk.red( ` You must upgrade node to >= ${packageConfig.engines.node} .x to use react-booster-cli` )) } request({ url: 'https://registry.npmjs.org/react-booster-cli', }, (err, res, body) => { if (! err && res.statusCode === 200) { const latestVersion = JSON.parse(body)['dist-tags'].latest const localVersion = packageConfig.version if (semver.lt(localVersion, latestVersion)) { console.log() console.log(chalk.yellow(' A newer version of booster-cli is available.')) console.log() console.log(` latest: ${chalk.green(latestVersion)}`) console.log(` installed: ${chalk.red(localVersion)}`) console.log() } resolve() }else{ reject() } }) }) }Copy the code
Command line function selection
The core is to use inquirer this package, to achieve the command line interface, this package is very powerful, provides a single option, multiple options, input box and other interactive ways, like a command line form ah!
lib/ask.js
const { prompt } = require('inquirer'); Const questions = [{name: 'platform', type: 'list', message: 'Which side does your Web application need to run on?', choices: [{name: 'platform', type: 'list', message:' which side does your Web application need to run on? Value: 'PC', 'PC'}, {name: 'mobile' value: 'mobile'}}, {name: 'isMPA, type:' list ', the message: Choices: [{name: 'SPA ', value: false,}, {name:' MPA ', value: true,}]}, {name: 'SPA ', value: true,}]}, {name:' SPA ', value: true,}]}, {name: 'stateLibrary', type: 'list', message: 'Which state management library do you want to install?', choices: [{name: 'mobx', value: 'mobx',}, {name: 'redux', value: 'redux',}]}, {name: 'reactRouterVersion', type: 'list', message: 'Select react-router version ', choices: [{name: 'v5', value: 'v5',}, {name: 'v6 ', value: 'v6', }] }, ] module.exports = function ask () { return prompt(questions) }Copy the code
Generate project files
lib/generate.js
const { isBinaryFileSync } = require("isbinaryfile"); const fs = require("fs"); const ejs = require("ejs"); const path = require("path"); /** * @name render file * @param {*} filePath filePath * @param {*} ejsOptions ejs inject data objects * @returns file content */ function RenderFile (filePath, ejsOptions = {}) {if (isBinaryFileSync(filePath)) {return fs.readfilesync (filePath); } const content = fs.readFileSync(filePath, "utf-8"); If (/[\\/] SRC [\\/].+/.test(filePath)) {return ejs.render(content, ejsOptions); } // Other files, such as webpack configuration files, directly read return content; } /** * @name generates project files * @param {*} answers collects questions * @returns file tree eg {'/path/a/b' : } */ async function generate(answers, targetDir) {const globby = require("globby"); Const fileList = await globby(["**/*"], {CWD: path.resolve(__dirname, ".. /template"), gitignore: true, dot: true, }); const { isMPA } = answers; // Ejs injected template variable const ejsData = {... answers, projectDir: targetDir, pageName:'index' }; Const filesTreeObj = {}; fileList.forEach((oriPath) => { let targetPath = oriPath; const absolutePath = path.resolve(__dirname, ".. /template", oriPath); If (isMPA && ^ / SRC / \ \ /] + /. The test (oriPath)) {/ / for more scenarios, generates multiple page template const/dir, file = oriPath. Split (/ / \ \ / + /); ["index", "pageA", "pageB"].forEach((pageName) => { targetPath = `${dir}/pages/${pageName}/${file}`; filesTreeObj[targetPath] = renderFile(absolutePath, { ... ejsData, pageName, }); }); } else { const content = renderFile(absolutePath, ejsData); filesTreeObj[targetPath] = content; }}); return filesTreeObj; } module.exports = generate;Copy the code
Inject arguments collected from the command line into ejS templates such as platform, which represents the Web platform.
When EJS renders,platform = mobile
, to select mobile, insert the head tag in the HTML templateflexible.js
The script.
After the functionality is developed, since most companies have their own private NPM libraries, the steps for delivering public packages are similar.
Release NPM package
npm login
lerna publish
Copy the code
This step, or quite easy to step on the pit, here is a summary of my encounter:
-
-
The name of the NPM exposed package needs to be unique.
It is a good idea to check the NPM website in advance to see if the package name you are sending already exists. Or you can buy a private domain, something like @vue/ XXX.
-
-
-
Lerna publish Retry does not take effect.
If git publish fails, git will not re-publish to NPM because git Tag has already been tagged.
To resolve this problem, run lerna publish from-git to publish the NPM package involved in the current tag. PS: NPM publish will not update package.json
-
-
- Global modification to taobao NPM source caused by the problem
-
There is a delay in the synchronization between Taobao source and official source
The NPM package has been released successfully, but because the global setting is taobao source, there will be a certain synchronization delay in the test, about half an hour to an hour, so the latest package version may not be updated or the package may not be found for a period of time after the first successful release.
-
Global use taobao source, will lead to packet failure.
Because taobao source can only download the package, can not upload the package. But without taobao source, installation of other dependence and slow. Solution: Install dependent, unified from taobao source pull, ensure dependent installation speed. Specify the official source in the package.json publishConfig field of the NPM package to ensure that the package is successfully shipped.
"publishConfig": { "registry": "https://registry.npmjs.org/", "access": "public" }, Copy the code
-
- Distinction between devDependencies and Dependencies
First, in a normal business project, there is no essential difference between the two. That is, if you install with –dev or not, it will only affect where you end up in package.json, and whether or not you end up being packaged and built by tools like Webpack, depends on whether or not it is referenced in the project.
But this is important for projects posted to NPM. When a user installs your package, only production dependencies are installed together; development dependencies are not. If used incorrectly, such as accidentally loading a production dependency into a development dependency, the user who installed your NPM package will run an error and cannot find the XX module.
The last
NPM address, Github address, welcome to try out, raise issues, this project is my spare time development, the above title and story is pure fiction. The code is also completely open source. If your team has similar needs, I can provide you with a reference. This is my best harvest.