$ npm i @chrise/cli -g
$ chrise create test
后面去看了@vue/cli的源码,不得不说,确实很正式很规范化。参考了@vue/cli 的部分功能,对脚手架进行修改。
项目chrise-react的结构分成了四个部分:
@chrise/cli脚手架命令行内容,通过命令去初始化项目等等操作。@chrise/scripts项目编译运行打包内容,暂未接入部署等流程。@chrise/template模板文件。@chrise/plugin-xxx项目封装的插件,例如@chrise/plugin-typescript等等。
之前是直接通过inquirer的一些交互命令,获取信息后,通过download-git-repo去对应github拉取模板文件,这样虽然比较简单,但是多个模板的话,维护就很难受了。
而在@vue/cli里,是通过多个插件组合成一个整体模板,在很多脚手架根目录下,都有一个xxx.config.js暴露出来。然后在运行node命令时,去读取配置文件,根据配置文件的内容去进行对应操作,例如:使用webpack-chain动态修改config,最后再调用toConfig去生成新的webpack配置内容。
一个基本的package.json模板,除了常规不变的version、private、license等等,像name,scripts,dependencies,devDependencies需要我们去手动添加进去。
name就使用脚手架初始化传入的参数,而scripts则是在成功引入@chrise/scripts后,使用其运行命令。
像一些必备的,例如react,react-dom,我们可以直接放到dependencies里,而devDependencies一般是初始化时,用户手动选择的plugins。
借助inquirer去罗列插件,让用户选择需要引入哪些插件。
constchrise_PLUGIN_CHECK=[{name: 'Typescript',value: ['tsx','@chrise/plugin-typescript'],},{name: 'Less',value: ['less','@chrise/plugin-less'],},];const{ plugins }=awaitinquirer.prompt([{type: 'checkbox',name: 'plugins',message: 'Do you need these plugins',choices: chrise_PLUGIN_CHECK,// 一个结构,自己觉得怎么处理方便,怎么来},]);那么根据上面的code就可以获取到,用户需要哪些plugin了,那么可以将这些plugin放入到json的devDependencies里。
上述无论是react还是plugin,都需要一个版本号,我这里是采用命令行去获取最新版本,然后作为其value值。如果直接遍历运行execSync的话,会阻塞住,ora的loading也要卡着不动,于是选择promise去运行,通过exec回调来结束promise。
constforEachSetV=(list,obj,key)=>{constpromises=[];constmanager=hasCnpm() ? 'cnpm' : 'npm';// 判断选择cnpm还是npmlist.forEach((item)=>{if(typeofitem==='object'){returnforEachSetV(item,obj,key);}constnewPromise=newPromise((res)=>{exec(`${manager} view ${item} version`,(err,stdout,stderr)=>{obj[key][item]=stdout.slice(0,stdout.length-1);res(0);});});promises.push(newPromise);});returnpromises;};constpromise=[
...forEachSetV(depe,pkg,'dependencies'),
...forEachSetV(devD,pkg,'devDependencies'),];awaitPromise.all(promise);那么就获取到版本号后,再将其他数据一同填入到json中,将其作为package.json的值,在新项目目录下,新建它。
constfs=require('fs-extra');// fs-extra是系统fs模块的扩展constpath=require('path');module.exports=(dir,files)=>{Object.keys(files).forEach((name)=>{constpathName=path.join(dir,name);fs.ensureDirSync(path.dirname(pathName));// 如果没有文件夹则新建文件夹fs.writeFileSync(pathName,files[name]);// 新建文件});};writeFileTree(targetDir,{'package.json': JSON.stringify(pkg,null,2),});因为npm的速度不甚理想,可以将其作为兜底处理。先判断当前环境中,是否有yarn、cnpm等等,然后优先选择前者,若都没有,则再使用npm进行操作。
constPM_CONFIG={npm: {install: ['install','--loglevel','error'],// 打印error信息remove: ['uninstall','--loglevel','error'],},yarn: {install: [],remove: ['remove'],},};PM_CONFIG.cnpm=PM_CONFIG.npm;module.exports=classPackageManager{constructor({ pkgName }){this.pkgName=pkgName;if(hasYarn()){this.bin='yarn';}elseif(hasCnpm()){this.bin='cnpm';}else{this.bin='npm';}}// 封装了下运行命令函数runCommand(command,args=[]){const_commands=[this.bin, ...PM_CONFIG[this.bin][command], ...args];execSync(_commands.join(' '),{stdio: [0,1,2]});}install(){try{this.runCommand('install',['--offline']);// offline指先去拉取缓存区里的,如果没有则去服务器拉}catch(e){this.runCommand('install');// 报错兜底}}git(){try{execSync('git init');returntrue;}catch(e){returnfalse;}}};而判断yarn和cnpm环境中是否存在,可以通过判断version等等方法,去看是否能够成功执行,若成功执行,则说明环境中存在,反之则否。
constjudgeEnv=(name)=>{constenvKey=`_has${name[0].toUpperCase()}${name.slice(1)}`;// 保存下结果if(_env[envKey]!==null){return_env[envKey];}try{execSync(`${name} --version`,{stdio: 'ignore'});// 不打印信息return(_env[envKey]=true);}catch(e){return(_env[envKey]=false);}};consthasYarn=judgeEnv.bind(this,'yarn');consthasCnpm=judgeEnv.bind(this,'cnpm');然后通过install方法去安装依赖,再将一些参数传递给@chrise/template去把一些基本模板复制过去。
因为是多页面项目,scripts里主要做了以下几件事情:
- 通过
glob去匹配入口,然后将其作为entry动态传入,并动态传入多个html-webpack-plugin给plugins。 - 通过读取项目根目录下的
chrise.config.js文件,来动态修改webpack配置内容并调用对应的插件。 - 最后生成最终的
webpack配置文件,传入给webpack去进行编译运行打包等等操作。
匹配入口主要使用glob去匹配,只有满足匹配要求,才作为入口。然后通过匹配到的信息,去生成对应的entry内容,和plugin内容,传递给webpack配置文件。
constSRC='./src/**/index.?(js|jsx|ts|tsx)';/** * get webpack entry */constgetEntries=()=>{if(entries)returnentries;entries={};constpages=glob.sync(SRC);pages.forEach((page)=>{// 遍历传entryconstdirname=path.dirname(page);constentry=path.basename(dirname);entries[entry]=page;});returnentries;};/** * get pages info * @param {Boolean} isProd */constgetPages=(isProd)=>{constplugins=[];letentries=getEntries();Object.keys(entries).map((dirname)=>{// 遍历传pluginplugins.push(newHtmlWebpackPlugin({chunks: isProd ? ['libs',dirname] : [dirname],filename: `./${dirname}/index.html`,template: path.join(__dirname,'./template/index.html'),}));});returnplugins;};链式配置推荐使用webpack-chain,@vue/cli也是使用它。因为我们原本就有一些基本配置内容,可以通过config.merge将我们已有的配置对象合并到配置实例中。
但是不支持直接转化,需要我们对某些配置内容,进行手动去转化,例如:module。而plugins不支持已经new的plugin,我这边的处理是跳过对plugin的合并,最后再使用webpack-merge将config.toConfig()和plugins再合并成最终的配置对象。
constConfig=require('webpack-chain');constchriseConfig=require(`${process.cwd()}/chrise.config.js`);// 读取根目录的配置文件const{ setBaseConfig }=require('./util/merge');// 将已有的配置文件对象合并到配置实例constBASE=require('./config/webpack.base');// 配置对象baseconstDEVE=merge(BASE,require('./config/webpack.deve'));// 配置对象 deveconstPROD=merge(BASE,require('./config/webpack.prod'));// 配置对象 prodconstconfig=newConfig();// 我这边就只是对plugin做一下处理,可以做其他很多事情,这里只是举个例子consthandlechriseConfig=({ plugins }={})=>{// to do sth.if(plugins){plugins.forEach((plugin)=>{require(plugin[0])(config,plugin[1]);});}};constgetConfig=(isDeve)=>{config.clear();// 清除配置setBaseConfig(isDeve ? DEVE : PROD,config);handlechriseConfig(chriseConfig);returnmerge(config.toConfig(),{plugins: isDeve ? DEVE.plugins : PROD.plugins,});// 最后再合并};在获取到webpack config后,那么可以根据是dev命令还是build命令,去调用对应的函数,进行编译运行打包等等操作了。(同理,根据program.command)
// dev 运行constwebpack=require('webpack');constWebpackDevServer=require('webpack-dev-server');const{ getDeveConfig }=require('./config');module.exports=()=>{constDEVE_CONFIG=getDeveConfig();const{ devServer }=DEVE_CONFIG;constcompiler=webpack(DEVE_CONFIG);constserver=newWebpackDevServer(compiler,{ ...devServer});server.listen(devServer.port);};// buildconstwebpack=require('webpack');const{ getProdConfig }=require('./config');constlogSymbols=require('log-symbols');module.exports=()=>{constPROD_CONFIG=getProdConfig();constcompiler=webpack(PROD_CONFIG);compiler.run((err,stats)=>{if(err){// 回调中接收错误信息。console.error(err);}else{console.log(logSymbols.success,'打包成功!');}});};template主要就是通过传入的参数,来判断是否要copy对应的文件,同时根据options来去修改对应文件内容和后缀。代码过于无趣,就不贴了。
plugin的话,可以做的事情比较多,我这边目前就只是来链式修改webpack配置信息。这只是其中一种功能,还有很多例如:自己写一个webpack plugin / loader去传入,去做一些其他事情。
// examplemodule.exports=(config)=>{['.tsx','.ts'].forEach((item)=>config.resolve.extensions.add(item));config.module.rule('js').test(/\.(js|ts|tsx|jsx)$/).use('babel-loader').tap((options)=>{options.presets.push('@babel/preset-typescript');returnoptions;});};