create-react-app
做为facebook官方的react脚手架是至关好用的。主要设计原理是将配置好的如Webpack,Babel,ESLint
,合并到react-scripts
这npm包中,用户就能够开箱即用。不少开发者都在这基础上进行改造开发。注意react-scripts
就是create-react-app脚手架的核心配置代码。css
目前若是要本身定制配置,有两种方案可选。一个是eject
,他的原理是将react-scripts
拆除而后将配置暴露到应用顶层,用户就能够自行进行配置。另外一个是使用react-app-rewired
,用户经过config-overrides.js
增长修改配置。二者各有好处。eject
直接暴露能够自行配置,可是坏处就是react-scripts
被解散了,就不能随官方配置进行升级。react-scripts
包揽了那些最基础配置的脏活累活,而且一直再维护,好比修复BUG和打包优化,运行速度优化。前端发展的迅速,这些基础配置随着基础设施的升级,可能随时都会变化。我以为eject
后要就须要承担维护成本的风险。个人理念是将专业的事情交给专业的人去作就行了,咱们应该享受金字塔底层带来的基础设施便利去创造价值,不必重复造轮子,更不必在轮子上耗费过多的维护成本。前端
个人理念是推荐使用config-overrides.js
来定制配置,下降维护成本。也就是在react-scripts
的配置上进行增删改查,不影响底层配置代码,在将来须要的时候还能够进行无缝升级react-scripts
,来提高速度或者解决你未关注到的BUG等等。可是create-react-app
只是提供最最基础的设施建设,咱们最经常使用的框架配置都须要本身去定制,每次建立项目的时候都须要再写一次定制代码,至关烦人。因此才有了今天的主题基于create-react-app的脚手架,确切说应该是基于react-scripts
的脚手架。node
因此这篇文章主题应该有两个react
react-scripts
来写脚手架项目核心代码在github上:(github.com/LinYouYuan/…),这个连接上面也有使用帮助说明,能够先点击进去看,能够更好的理解使用和需求。webpack
咱们需求是:ios
第一点,咱们须要引入react-scripts
和react-app-rewired
,来保持官方同步和可定制型。git
第二点,我整理出咱们经常使用的框架可选项:github
类型 | 可选框架名称 |
---|---|
语言 | JavaScript / TypeScript |
状态管理库 | Redux / Mobx |
css预处理器 | SCSS / LESS / styled-components |
UI组件 | Antd / Ant-mobile |
代码规范 | Airbnb |
HTTP库 | Axios |
路由 | react-router |
第三点,创项目后咱们能够经过config-overrides.js
文件来预先配置,而后用户能够再此文件进行继续配置和改造。web
首先建立nodejs项目。制做经常使用的Cli工具,咱们通常都须要安装下面5个工具包:(执行npm install
或者其余工具安装)npm
child_process
;咱们首先要建立一个像creact-react-app
同样直接在全局就能够执行使用的命令。
lib/index.js
,这个其实就是入口执行文件。其中#!/usr/bin/env node
必定要填写。lib/index.js
#!/usr/bin/env node
console.log('hello world')
复制代码
package.json
中添加代码,以下,其中react-cli
就是全局要使用的命令名称,lib/index.js
就是上面要执行的文件地址。package.json
"bin": {
"react-cli": "lib/index.js"
}
复制代码
执行npm link
。执行完成后,咱们就能够把命令挂载到全局,效果和npm install -g
后同样,能够全局输入命令。link的主要目的是给我开发调试用的。如今能够直接在控制台输入react-cli
执行,你就能够看到打印的hello world
了。
等开发完成,你能够试试发布到npm包上,可是我推荐等开发完成后再发布,固然不妨碍你好奇心想试试。发布前须要执行npm login
,登陆npm帐号密码,注意你若是是淘宝源你须要经过npm config set registry http://registry.npm.tongdun.cn
暂时切回官方源。而后执行npm publish
发布,这个时候也要注意,你的package.json
中的name
也就是项目名称不要和别人重名了。发布好你就能够经过npm i <you project name> -g
来全局安装你的包。
在lib/index.js
中,咱们输入以下
const program = require('commander');
const chalk = require("chalk");
program
.version(require('../package').version)
.usage('<command> [options]');
program
.command('create <app-name>')
.description('create a new project powered by react-cli')
.action(name => {
// 这里处理逻辑
console.log(chalk.blue(`React CLI v${require('../package').version}`));
// const create = require('./cli/create');
// create(name);
});
复制代码
这里主要经过commander
来配置接受不一样命令处理。这里主要就是要接受create <app-name>
参数,而后处理输入命令后的逻辑。其中chalk
就是颜色处理。
而后继续处理未输入和输入错时候弹出帮助以下
program
.arguments('<command>')
.action((cmd) => {
program.outputHelp()
console.log(` ` + chalk.red(`Unknown command ${chalk.yellow(cmd)}.`))
console.log()
})
program.parse(process.argv);
if (!program.args.length) {
program.outputHelp();
}
复制代码
接收到用户输入的命令后,咱们就要呈现交互界面,这个时候咱们就用到了很是好用的工具inquirer
。具体能够实现多少种交互形式能够点inquirer的npm网站的介绍看。我这里主要用了list
和confirm
的功能,也就是列表选择和寻问功能。好比让用户选择使用什么框架:
function selectManually(appName) {
inquirer
.prompt([
{
type: 'list',
name: 'language',
message: 'pick a language:',
choices: [
'JavaScript',
'TypeScript',
]
},
{
type: 'list',
name: 'stateManagement',
message: 'Pick a state management:',
choices: [
'Mobx',
'Redux',
]
},
{
type: 'list',
name: 'cssPre',
message: 'Pick a CSS pre-processor:',
choices: [
'LESS',
'SCSS/SASS',
'styled-components',
]
},
{
type: 'list',
name: 'design',
message: 'Pick a UI Design:',
choices: [
'Ant Design',
'Ant Design Mobile',
]
},
])
.then(answers => {
const creator = new Creator(appName, answers);
creator.create();
})
}
复制代码
新建一个Creator类,主要用来建立项目用的,初始化接受两个参数,一个是项目名称,一个是用户选择的框架。我项目中的模板存放在lib/packages/common-default
中。这里我主要针对各类不一样的配置,来修改packages.json
、babelrc
、config-overrides.js
文件的内容就行了,而后执行复制操做。
const chalk = require("chalk");
const fs = require("fs-extra");
const path = require("path");
const inquirer = module.require('inquirer');
const {
getPackageJson,
writeJsonToApp,
copyFiles,
setNewPackageVersion,
installPackge,
setUserConfig,
} = require('../packages/common');
class Creator {
constructor(appName, answers) {
this.appName = appName;
this.answers = answers;
this.appDir = path.resolve(process.cwd(), this.appName);
this.package = getPackageJson('cli-switch');
this.babelrc = {
plugins: [
[
"import",
{
libraryName: "antd",
style: true,
}
]
]
}
}
async testExistDir() {
if (fs.existsSync(this.appDir)) {
const { override } = await inquirer.prompt([
{
type: "confirm",
name: "override",
message: chalk.red(`directory ${this.appName} exist,override it?`)
}
]);
if (override) {
console.log(chalk.green("removing..."));
fs.removeSync(this.appDir);
return true;
} else {
process.exit(1);
return false;
}
}
return true;
}
async create() {
const { stateManagement, cssPre, design } = this.answers;
console.log();
console.log(`you pick: ${chalk.yellow(`${stateManagement}, ${cssPre}, ${design}, Router, ESLint`)}`);
console.log();
const isOk = await this.testExistDir(this.appDir, this.appName);
if (!isOk) {
return;
}
console.log(`🚀 Invoking generators...`);
console.log();
let { dependencies, devDependencies } = this.package;
switch (stateManagement) {
case 'Mobx':
dependencies['mobx'] = '';
dependencies['mobx-react'] = '';
break;
case 'Redux':
devDependencies['redux-devtools'] = '';
dependencies['redux'] = '';
dependencies['react-redux'] = '';
break;
}
switch (design) {
case 'Ant Design':
let myTd = this.babelrc.plugins[0][1];
myTd.libraryDirectory = 'es';
dependencies['antd'] = '';
break;
case 'Ant Design Mobile':
let myTdw = this.babelrc.plugins[0][1];
myTdw.libraryName = 'antd-mobile';
myTdw.style = 'css';
dependencies['antd-mobile'] = '';
break;
}
switch (cssPre) {
case 'LESS':
dependencies['less-loader'] = '';
devDependencies['react-app-rewire-less-modules'] = '';
break;
case 'SCSS/SASS':
dependencies['node-sass'] = '';
break;
case 'styled-components':
dependencies['styled-components'] = '';
devDependencies['babel-plugin-styled-components'] = '';
this.babelrc.plugins.push("babel-plugin-styled-components");
break;
}
fs.mkdirSync(this.appDir);
this.beginCopy(cssPre === 'LESS');
writeJsonToApp(this.appDir, '.babelrc', this.babelrc);
console.log(`📦 Installing additional dependencies...`);
installPackge(this.appDir);
setUserConfig({ hasConfig: true, config: this.answers });
console.log(`🎉 Successfully created project ${chalk.yellow(this.appName)}.`)
process.exit(1);
}
async beginCopy(isLess = false) {
setNewPackageVersion(this.package.dependencies);
setNewPackageVersion(this.package.devDependencies);
this.package.name = this.appName;
copyFiles(path.join(__filename, '../../packages/common-default'), this.appDir);
writeJsonToApp(this.appDir, 'package.json', this.package);
if (!isLess) {
fs.copySync(path.join(__filename, '../../packages/cli-switch/config-overrides.js'), this.appDir + '/config-overrides.js');
}
}
}
module.exports = Creator;
复制代码
建立好项目只要在config-overrides.js
里配置Webpack devServer jest。能够在这里添加自定义的config配置来增长修改loader, plugin, optimization进行配置。webpackMerge
使用混入的方式去添加config。
config-overrides.js
const path = require('path');
const webpackMerge = require('@/webpack-merge');
const appSrc = path.join(__dirname, 'src');
SKIP_PREFLIGHT_CHECK = true
const {
override, addLessLoader, addWebpackAlias, useBabelRc, addDecoratorsLegacy,
} = require('@/customize-cra');
//打包分析
const BundleAnalyzerPlugin = require('@/webpack-bundle-analyzer').BundleAnalyzerPlugin;
// 这里能够直接修改 Host 或者 Port
// process.env.HOST = 'localhost.xxxx.com';
// process.env.PORT = 3006;
// 生产环境是否打包 Source Map
process.env.GENERATE_SOURCEMAP = false;
module.exports = {
// 配置devServer
devServer: configFunction => (proxy, allowedHost) => {
proxy = {
'/mock': {
// 这里配置代理服务地址
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: { '^/mock': '' },
},
}
// allowedHost: 添加额外的地址
const config = configFunction(proxy, allowedHost);
return config;
},
// 配置webpack
webpack: (config, env) => {
// 开发环境
const isEnvDevelopment = env === 'development';
// 生产环境
const isEnvProduction = env === 'production';
// 经过customize-cra插件覆盖
config = override(
// 配置路径别名
addWebpackAlias({ '@': appSrc }),
// 对Decorators支持
addDecoratorsLegacy(),
useBabelRc(),
)(config, env);
return webpackMerge(config, {
// 用户能够在这里添加自定义的config配置 来增长修改loader, plugin, optimization
plugins: [
// new BundleAnalyzerPlugin(),
],
optimization: {
splitChunks: {
cacheGroups: {
vendors: { // 基本框架
chunks: 'all',
test: /(react|react-dom|react-dom-router|babel-polyfill|mobx|antd)/,
priority: 100,
name: 'vendors',
},
asyncCommons: { // 其他异步加载包
chunks: 'async',
minChunks: 2,
name: 'async-commons',
priority: 90,
},
commons: { // 其他同步加载包
chunks: 'all',
minChunks: 2,
name: 'commons',
priority: 80,
},
// echartsVendor: { // 异步加载echarts包
// test: /echarts/,
// priority: 100, // 高于async-commons优先级
// name: 'echartsVendor',
// chunks: 'async'
// },
}
},
}
})
},
// 配置测试
jest: config => {
config.moduleNameMapper = {
// 同webpack同样配置别名
'@/(.*)$': '<rootDir>/src/$1',
}
return config;
},
}
复制代码
用户第一次建立有两个选项
default (JavaScript, Redux, Antd, Less, Router, ESLint)
默认配置Manually select features
选择配置第二次建立的时候会多一个用户上次选择过的选项配置config
,就像以下进行选择配置。