首页 » PHP教程 » phplibeventepoll技巧_leader要我三天时间搭一套reactcli出来我准许了

phplibeventepoll技巧_leader要我三天时间搭一套reactcli出来我准许了

访客 2024-12-14 0

扫一扫用手机浏览

文章目录 [+]

原文:https://juejin.cn/post/7033959447017816077

如有侵权,联系删除

phplibeventepoll技巧_leader要我三天时间搭一套reactcli出来我准许了

phplibeventepoll技巧_leader要我三天时间搭一套reactcli出来我准许了
(图片来自网络侵删)
序言

逐渐的,我们都变成了当初最讨厌的那个人(标题党)。

但比较形式,内容明显是更主要的。
希望读者通过本文,能完全的体验到: 从零到一开拓一款脚手架的心途经程。

调研&谈论

虽然项目也没啥受重视的,但项目流程该有还是要有的。
首先通过安利拉了俩个成员入伙。
大略跟大家开会沟通了一下:

1. 市情主流方案调研

首先,提到react脚手架,一定离不开在react社区霸占主流的create-react-app,但出乎猜想的被大家第一韶光就打消了。
紧张是由于它配置不足灵巧,对webpack的封装太去世,这让它在应对繁芜项目的时候,有点力不从心,而且自身供应的功能虽然说很通用但是也足够简陋。
我猜这时候一定会有人会说,你可以eject一下,把源码暴露出来啊?但这样的话,我利用它的意义就没有了,而且基于它的源码二次开拓,本钱肯定低不了。

接下来,我们把目标转向了vue-cli,没想到得到了大家的同等好评。
我认为它最精良的地方是于: 在功能的通用性及灵巧性,这两个指标上取得了一个很好的平衡。
这点是与vue.js框架的哲学是高度同等的,以是与vue结合起来开拓项目,会有一种行云流水的觉得,而且大家同等对它的cli(交互命令行工具)喜好有加,便是下边这个东东,用过的人一定印象深刻。

要说它有啥大毛病没有,想了想,不支持react算不算?

那么,我们的目标也就有了,开拓一款利用办法类似于vue-cli的react脚手架。
之前对@vue/cli的认识只勾留在利用层面,以是须要先对它的实现理解一波。
正所谓,知自知彼,才好实现(抄)嘛。

2. @vue/cli源码剖析

我clone的是4.5.14[1]的版本,5.0新版本起笔时,而且还处在beta版阶段,略过。

整体的代码是利用lerna + yarn-workspace掩护的,而且与vue正式功能有关的包都包含在了@vue这个npm域(有人也称为npm组织)下。

如上图所示,完全的@vue/cli功能紧张是三部分组成的:

@vue/cli 卖力命令行参数网络@vue/cli-service 这个包是启动vue的引擎及核心,承载webpack配置。
@vue/plugin-xxx vue-cli的插件,一个插件对应一个功能,与上边用户自定义feature选项逐一对应。
cli与核心功能包分离是一种主流的拆包办法webpack-cli & webpack,@babel/cli & @babel/core皆是如此实现的,属于程序设计上的分层架构,cli(上层)须要依赖核心做事(下层),但核心做事(下层)并不依赖cli(上层),依然可以独立运行。

阅读源码第一步,先从package.json入手。
这种通过命令利用的包,都是通过package.json的 bin[2] 字段来实现。

"bin": { "vue": "bin/vue.js" },

顺藤摸瓜,打开bin/vue.js,这里 这里利用Commander这个命令行解析工具,注册了一些全局命令,这里只把稳create这个命令就OK。

在实行全局的vue create xxx命令时,运行的便是这个文件。

当vue create xxx命令触发时,会实行../lib/create这个文件,会把项目名称name传进去,连续追踪../lib/create.js。

在create主函数中。
首先,拼一个将要创建项目的目标路径targetDir(这里大部分情形是,敲vue create命令时的所在目录 + 传进来的项目name)。

紧接着,是对磁盘已存在同名项目的处理。
接下来是重点,new Creator 这个类正式开启了创建项目的主流程。

找到这个Creator类,可以创造它继续了node的EventEmiter这个事宜处理类,这点是为了实现它的全体插件机制,这类似于在编写webpack的插件过程中,须要订阅一些内部派发的一些hook,实现基于发布订阅的事宜模式,下边会细细讲解这块。

往下就须要开启断点调试了,帮助将它的全体运行流程看的更清楚些。

左侧红框中的交互选项是不是时曾相似,没错,它对应的交互界面,在上边涌现过。

vue为了方便用户选择,预设了一些功能凑集,Creator类的constuctor里只是初始化一些属性变量,核心的是紧接着调用的create实例方法。

调用promptAndResolvePreset ,会弹出预设选项界面,✅ 一个默认选项,连续;

左侧可以看到presets这个表示所选预设的数据里,已经包含了预设里包含的babel、 eslint等插件。

换一个预设,选择手动呢?如下图:得到也是相同的数据构造。

接下来,便是给presets里的plugins数组里的每一个插件,初始化一些默认选项

接下来,初始化天生 package.json 须要的数据。

将刚才预设里包含的插件,插入devDependencies开拓依赖中。

插入成功,调试栈里已经可以看到包含了3个开拓依赖

接下来,将package.json写入磁盘。

实行npm install,安装依赖。

接着,又做了些:写入npmrc、yarnrc文件,初始化git仓库等一些无关紧要的事情,快速掠过。

重点来了,下面就要利用刚安装好的插件,天生项目模版了。

this.resolvePlugins(preset.plugins, pkg)这个方法很关键,用来引入插件。
但在先容这个方法之前,我想先聊一聊框架的插件机制以及它在vue/cli中的设计与实现。

插件机制:

便是可以将一些可选配或者用户须要自定义的功能封装起来,按需插入利用,这样的设计上风是:可以拆分代码繁芜度和功能耦合度,并且极大的增加了框架的可扩展性。

插件,首先要定好怎么插。
这也是插件机制最主要的一点,也便是框架开拓者与插件开拓者须要约定好一套插件接入的固定办法。
表示在vue/cli中,这种约定如下图所示。

每一个插件都有一个generator文件夹,下边必须存在一个index.js,然后index.js须要导出一个函数,在函数体里可以调用外部主体注入的一些工具方法,比如,

api.injectImport() 在项目里插入一个模块导入。
api.extendPackage() 扩展项目的package.jsonapi.render('./template') 利用ejs渲染模版文件(条件编译)

再回来看上边的this.resolvePlugins(preset.plugins, pkg)方法,这里取到的apply,实在便是插件约定导出的那个函数,但这里须要把稳的是,只是暂时将apply方法保存起来,并不会调用。

原plugins,经由此方法处理后,就转换为了新的包含详细插件功能的plugins。
{ id: options } => [{ id, apply, options }]

接下来,将整理好的plugins传入Generator这个类中,开始了末了一步,也是最本色的一步:天生项目。

可以这样说,之前一大堆的参数网络、整理、引入插件等准备事情都是为了终极的天生这一步。

new 之后,紧接着调了generate这个实例方法,以是接下来,重点看下这个方法的实现

方法实行,首先调用了this.initPlugins(),这个方法中最主要的便是遍历实行所有插件默认的apply方法(即前边提到的插件机制中约定默认导出的那个函数),apply第一个参数是api,包含了render渲染ejs模版等这些能力,而这些能力都来自GeneratorAPI这个类,把稳第二个参数,会把当前this传入。

接下来,连续看GeneratorAPI的实现。
先打消下@vue/cli-service这个包,由于这个包虽然存在于plugins数组中,但它却很分外,它属于核心做事,严格意义上并不属于插件的范畴,不须要当插件处理,只需参与安装就行,这里我猜只是让数据构造只管即便保持大略,用时候过滤一下也没有很麻烦。

GeneratorAPI的整体实现采取了一种中间件的设计,其上的 _injectFileMiddleware 方法,会将api上的方法调用以push一个函数的办法先存起来,暂不实行。

拿上边讲到的api.render()渲染模版方法举例,在调用插件导出的apply方法时,确实会触发api.render()调用,但却并不会真正的利用ejs编译模版文件,而是将他们暂存到this.generator.fileMiddlewares这个中间暂存数组中。

那么api.render()这些方法真正的起浸染是在哪里呢?

打开紧接着实行的 this.resolveFiles 方法,原形就在这里。

用for of串行调用每一个middleware函数,得到终极须要天生的文件内容

此刻,this.files 就保存了完全的文件路径到文件内容的映射,如上图左侧所示。

接下来,是一些常规操作。
sortPkg 可以对package.json的依赖整理下顺序,知足下一些强制症er的感想熏染。

接下来就可以根据this.files,调用 writeFileTree 往磁盘愉快的写文件了。

最终生成的目录构造如下,流程先容完毕。

3. 确定功能及目标

前边的对@vue/cli的详细剖析,可以总结为关键的三点

采取命令行界面的进行功能选择cli 与 核心做事 @vue/cli-service(webpack配置)采取分层设计,独立发包插件机制,按需天生不同的功能模版文件

这样的设计对一个面向所有vue开拓者的脚手架而言是必要的,由于它须要具备非常大的灵巧性。
但对一款只知足特定团队的脚手架来说,是过于繁芜的,而且开拓本钱也会很高,光是将各种功能插件拆分就须要非常大的事情量。
其次,团队的项目开拓,最主要的便是统一与规范,很多配置及功能都可以是默认内置的,比如eslint、babel、css处理器,这些都是团队项目开拓中必备的功能,以是配置的多样性及灵巧性并不是紧张考虑成分。
以是,第三点插件机制可以舍去。

cli与webpack配置分离,这样可以做到两者的独立版本更新,有利于用户项目的持续掩护,但这样做的本钱在于须要自己定义一套与webpack配置大不一样的配置约定出来,这个在@vue/cli-service 中是利用vue.config.js作为配置文件,其他主流的脚手架比如create-react-app都是走的这个路子。
这样的话,就还须要有一份详细的脚手架配置的文档,通过配置脚手架去配置webpack,这就导致我们已节制的webpack配置知识无用武之地,vue中采取的chainWebpack是个办理办法,但究竟它有多难用,只有用过的人才能体会到。
我们大概只须要一款透明的webpack配置,所有的rules、plugins都是可以直接修正的,这样就不须要文档,只须要会webpack配置就能搞定统统,而且,这份webpack配置在我们的团队便是现成的,以是上边提到的第二点也不须要。

以是,我们须要借鉴的地方就紧张在于交互命令行工具,还有按需进行ejs模版编译。
同时结合我们团队的项目特点进行功能提炼,经由小伙伴的谈论,紧张有以下几点选择:

(1) 移动端还是PC端?

PC端会内置基于antd的布局模版,移动端会开启px2rem适配,html里内联 flexible.js脚本。

(2) 天生单页or多页模版?

MPA与SPA的需求在我们的项目场景中都是存在的,以是在脚手架层面去区分两者还是很故意义的。

(3) 状态管理库、react-router版本按需安装

功能开拓

初始化项目

monorepo 它比较单仓库的上风是在于有利于多个npm之间的高效联调及node_modules磁盘空间共享 由于lerna的依赖提升对依赖版本号哀求过于严格,而且短缺很多yarn-workspace独占的功能(特殊是对bin、module确当地软链处理)。
目前主流的实践是,利用yarn的workspace做依赖管理,lerna做npm包的自动版本管理及发包。
这里采取monorepo的缘故原由是,后续可能会基于这个脚手架开拓组件库或者webpack-plugin等项目,方便他们之间的联调。

npm i -g lernalerna init

lerna.json

{ "packages": [ "packages/" ], "version": "0.1.6", "npmClient": "yarn", "useWorkspaces": true}

package.json

... "workspaces": [ "packages/" ], ...

创建cli包

lerna create react-booster-cli

初始化下项目

yarn install

接下来便是实现cli的功能,首先在剖析vue/cli源码时提到了,全局包用到的全局命令,是通过package.json的bin字段实现的,咱们也一样。

booster/packages/react-booster-cli/package.json "bin": { "booster": "bin/booster.js" },

接下来创建./bin/booster.js文件

#!/usr/bin/env node// 命令行解析工具const program = require('commander');program .version(require("../package").version) .usage("<command> [options]"); program.command("create <project-name>") .description("创建一个新的项目") .action((projectName)=>{ require('../lib/create')(projectName) }) program.parse(process.argv);

#!/usr/bin/env node这句非常主要,声明此文件须要利用node程序来实行。
里边用到了commander这个命令行参数解析库,安装一下

yarn workspace react-booster-cli add commander

现在就可以测试跑通下咱们的流程了,在booster根目录实行

npx booster

涌现以下界面就算成功了

大概有人会好奇运行成功的缘故原由,我这里大略阐明下,首先,npx xxx会先去当前目录下的node_modules/.bin/目录下找xxx文件。
很显然,是存在的。
这是为什么呢? 虽然声明了全局命令,但写的cli包,一没发包,二没安装。

实在这是yarn install 的一个附带(超好用的)功能,准确说是通过利用workspace,yarn install会自动的帮忙办理安装和link问题,事理是在node_modules/.bin目录下创建软链(类似于win上的文件快捷办法),链接到了packages/react-booster-cli/bin/booster.js。

接下来实行 create 命令会走comander的action,将项目名传到create文件里的create方法中,这与vue/cli是同等的

功能实现

实现create方法

这里须要很多做cli常用的一些工具包,每个工具的用场下边都有注释解释,很多实在都是用来义务令行更加都雅的。
大概npm生态里大部分都是前端开拓者,像这样装饰命令行的包种类和数量都格外丰富。
比如可以利用ora、chalk很方便实现一些命令行加载效果、颜色字体以及进度条,增加命令行的用户体验。

lib/create.js

const path = require("path");const fs = require("fs");// 检测目录是否存在const exists = fs.existsSync;// 删除文件const rm = require("rimraf").sync;//讯问cli输入参数const ask = require("./ask");// 命令行交互工具const inquirer = require("inquirer");// 命令行loadingconst ora = require("ora");// 输出增色const chalk = require("chalk");// 检测版本const checkVersion = require("./check-version");const generate = require("./generate");const { writeFileTree } = require("./util/file");const runCommand = require("./util/run");// loadingconst spinner = ora();async function create(projectName) { const cwd = process.cwd(); //当前运行node命令的目录 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 { // 网络用户输入选项 const answers = await ask(); spinner.start("check version"); // 检测版本 await checkVersion(); spinner.succeed(); console.log(`✨ Creating project in ${chalk.yellow(projectPath)}.`); // console.log(answers); // 更新 package.json const pkg = require("../template/package.json"); // 天生项目配置文件,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(); console.log(` Initializing 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` + chalk.cyan(` ${chalk.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); });};

检测版本

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() } }) })}

命令行功能选择

核心是利用inquirer这个包,来实现命令行交互界面,这个包非常强大,供应了单选、多选、输入框等交互办法,活脱脱一个命令行form啊!

lib/ask.js

const { prompt } = require('inquirer');//天生命令行交互界面const questions = [ { name: 'platform', type: 'list', message: '您的web运用须要运行在哪个端呢?', choices: [{ name: 'PC端', value: 'pc', }, { name: '移动端', value: 'mobile', }] }, { name: 'isMPA', type: 'list', message: '天生单页or多页模版?', choices: [{ name: '单页(SPA)', value: false, }, { name: '多页(MPA)', value: true, }] }, { name: 'stateLibrary', type: 'list', message: '您希望安装的状态管理库是?', choices: [{ name: 'mobx', value: 'mobx', }, { name: 'redux', value: 'redux', }] }, { name: 'reactRouterVersion', type: 'list', message: '选择react-router版本', choices: [{ name: 'v5(推举)', value: 'v5', }, { name: 'v6(对hook支持度较好,但api不足稳定)', value: 'v6', }] },]module.exports = function ask () { return prompt(questions)}

天生项目文件

lib/generate.js

const { isBinaryFileSync } = require("isbinaryfile");const fs = require("fs");const ejs = require("ejs");const path = require("path");/ @name 渲染文件 @param {} filePath 文件路径 @param {} ejsOptions ejs注入数据工具 @returns 文件内容 /function renderFile(filePath, ejsOptions = {}) { // 二进制文件直接返回 if (isBinaryFileSync(filePath)) { return fs.readFileSync(filePath); } const content = fs.readFileSync(filePath, "utf-8"); //src目录下须要经由ejs动态编译 if (/[\\/]src[\\/].+/.test(filePath)) { return ejs.render(content, ejsOptions); } // 其他文件,比如webpack的配置文件,直接读取返回 return content;}/ @name 天生项目文件 @param {} answers 网络的问题 @returns 文件树 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注入的模版变量 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[\\/].+/.test(oriPath)) { // 针对多页场景,天生多页面模版 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;

将命令行网络到的参数注入到ejs模版中

比如在命令行通过用户交互网络到platform这个代表web平台的参数。

ejs渲染时, platform = mobile,代表选择是移动端,就在html模版中的head标签中插入flexible.js的脚本,PC的话,就不须要。
这样就可以将不同功能的最终生成代码区分开。

功能开拓完毕之后,由于大部分公司都是有自己的私有npm库的,我这里演示下发公开包的步骤,实在都差不多。

发布npm包

npm loginlerna publish

发包这一步,还是挺随意马虎踩到坑的,这里总结下我碰着的:

npm公开包的名称须要是唯一的。
最好提前去npm[3]网站搜索一下,看自己将要发的包名是否已存在。
或者费钱买私有域,类似于@vue/xxx这样的。
lerna publish重试不生效。
运行lerna publish如果中途有包发布失落败,再运行lerna publish的时候,由于git的Tag已经打上去了,以是不会再重新发布包到NPM。
办理方法:运行lerna publish from-git,会把当前标签中涉及的NPM包再发布一次,PS:不会再更新package.json,只是实行npm publish淘宝源与官方源的同步存在延时npm包已经发布成功,但由于全局设置的是淘宝源,亲测会有一定的同步延时,大概半小时到一个小时旁边,以是有可能会更新不到最新的包版本或者直接首次发版成功后的一段韶光内找不到包。
全局用淘宝源,会导致发包失落败。
由于淘宝源只能下载包,不能上传包。
但是不用淘宝源,安装其他依赖又慢的弗成。
办理方法:安装依赖,统一从淘宝源拉,担保依赖安装速率。
发包时借助 npm包的package.json的publishConfig字段指定官方源,确保能发包成功。
"publishConfig": { "registry": "https://registry.npmjs.org/", "access": "public" },全局修正为淘宝npm源导致的问题但对付发到npm上的项目而言,这点很主要。
当用户安装你的包时,只有生产依赖才会被一起安装,开拓依赖则不会。
如果利用不当,比如欠妥心将一个生产依赖装入了开拓依赖,安装你npm包的用户会运行报错,找不到xx模块。
devDependencies与dependencies的差异 首先,在普通的业务项目中,这两者是没有任何实质差异的。
也便是说在安装时,带不带--dev,只会影响终极在package.json中的归类位置,终极会不会被webpack等的工具打包构建,只决定于是否在项目中被引用。

标签:

相关文章

php多个if语句技巧_PHP IfElse 语句

PHP 条件语句当您编写代码时,您常常须要为不同的判断实行不同的动作。您可以在代码中利用条件语句来完成此任务。在 PHP 中,供应...

PHP教程 2024-12-16 阅读0 评论0

大数据资产,新时代企业核心竞争力之钥

随着信息技术的飞速发展,大数据已成为推动社会进步的重要力量。在新时代,企业如何有效利用大数据资产,成为提升核心竞争力、实现可持续发...

PHP教程 2024-12-16 阅读0 评论0