标签:rail 更新 glob during 动画 smo mesa fine jpg
扔掉Create React App,打造你自己的React生成工具!
作者|Sviat Kuzhelev
译者|无明
每个人都喜欢现成的东西。很显然,对于基于 React 的代码生成系统来说,没有什么比 Facebook 团队推出的 create-react-app 更好的了。是的,它非常有用。有了它,你可以立马开始 App 的编码工作。但从另一面来看,这种方式也让我们失去了了解内部工作原理的机会。我们应该要透过美丽的高级 API,了解它的内在机制。所以,今天就让我们来尝试构建自己的第一个 React Starter Kit Builder(RSK)!
在这篇文章中,我将使用我的全新 RSK Builder,你可以从 GitHub 上克隆:
https://github.com/BiosBoy/coconat
让我们回到最初,从我们的想象开始。我们什么都没有,只有笔记本电脑和我们已经掌握的牢固的 JS 知识。为了增加趣味,我们也可以说我们需要创建一个抽象的真实世界的商业 App,我们需要实现很多现代功能,例如:
yarn init
在这一步,CLI 中有一些繁琐的字段需要填写。假设你已经填好了,现在,和往常一样,我们得到了一个 App 初始化文件夹。我们可以开始安装所有必需的 npm 包和 Webpack。
yarn add webpack
只安装一个 Webpack 包还不够,还要安装其他的一些必需的软件包:
yarn add webpack-cli webpack-bundle-analyzer browser-sync-webpack-plugin clean-webpack-plugin html-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin terser-webpack-plugin webpack-dev-middleware webpack-dev-server webpack-hot-middleware
这里有很多东西,但不要害怕,我不打算详细说明每个包,因为这样可能会让你感到紧张。简单地说,所有这些包都将帮助我们输出更好的代码。
我们先从在根文件夹中创建和配置 webpack.config.js 文件开始。你需要了解的是,任何 webpack 配置文件都包含了 3 个主要配置项:
// import global vars for a whole app
require(‘./globals‘);
const path = require(‘path‘);
const webpack = require(‘webpack‘);
const HtmlWebpackPlugin = require(‘html-webpack-plugin‘);
const MiniCssExtractPlugin = require(‘mini-css-extract-plugin‘);
const UglifyJsPlugin = require(‘uglifyjs-webpack-plugin‘);
const OptimizeCSSAssetsPlugin = require(‘optimize-css-assets-webpack-plugin‘);
const { BundleAnalyzerPlugin } = require(‘webpack-bundle-analyzer‘);
const debug = require(‘debug‘)(‘app:webpack:config‘);
在理解了 webpack 的基本逻辑后,就可以开始编写配置了,我们将从规则开始。
规则
// ------------------------------------
// RULES INJECTION!
// ------------------------------------
const rules = [
// PRELOAD CHECKING
{
enforce: ‘pre‘,
test: /\.(js|jsx)?$/,
exclude: /(node_modules|bower_components)/,
loader: ‘eslint-loader‘,
options: {
quiet: true
}
},
{
enforce: ‘pre‘,
test: /\.(ts|tsx)?$/,
exclude: /(node_modules|bower_components)/,
loader: ‘tslint-loader‘,
options: {
quiet: true,
tsConfigFile: ‘./tsconfig.json‘
}
},
// JAVASCRIPT/JSON
{
test: /\.html$/,
use: {
loader: ‘html-loader‘
}
},
{
test: /\.(js|jsx|ts|tsx)?$/,
exclude: /(node_modules|bower_components)/,
loader: ‘babel-loader‘
},
{
type: ‘javascript/auto‘,
test: /\.json$/,
loader: ‘json-loader‘
},
// STYLES
{
test: /.scss$/,
use: [
__PROD__ ? MiniCssExtractPlugin.loader : ‘style-loader‘,
{
loader: ‘css-loader‘,
options: {
importLoaders: 2,
modules: true,
localIdentName: ‘[local]___[hash:base64:5]‘
}
},
‘postcss-loader‘,
‘sass-loader‘
]
},
// FILE/IMAGES
{
test: /\.woff(\?.*)?$/,
loader: ‘url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff‘
},
{
test: /\.woff2(\?.*)?$/,
loader: ‘url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff2‘
},
{
test: /\.otf(\?.*)?$/,
loader: ‘file-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=font/opentype‘
},
{
test: /\.ttf(\?.*)?$/,
loader: ‘url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/octet-stream‘
},
{
test: /\.eot(\?.*)?$/,
loader: ‘file-loader?prefix=fonts/&name=[path][name].[ext]‘
},
{
test: /\.svg(\?.*)?$/,
loader: ‘url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=image/svg+xml‘
},
{
test: /\.(png|jpg)$/,
loader: ‘url-loader?limit=8192‘
}
];
这里有一些有趣的东西:
1.我们有 js/jsx/ts/tsx 文件的预加载器。我们可以使用一些特殊的工具(比如 js/ts-linters)来预先处理代码中的错误,在它们破坏我们的 App 之前捕获它们。
2.我们有 js/jsx/ts/tsx/sccs/html/json 和其他常规文件加载器。有了这些加载器,webpack 可以通过各种方式处理我们的 App。所以,我们可以看到配置中有样式加载器部分,其中三元表达式规定了如何加载样式文件:在生产阶段使用 MiniCssExtractPlugin 插件,在开发阶段使用自定义样式加载器,这样可以让我们的代码更加扁平化。
捆绑包优化
webpack 配置中的下一个重要步骤是清楚地了解如何有效地优化 App 代码的输出。好在这不是很难,我们只需填写三个重要的属性:
// ------------------------------------
// BUNDLES OPTIMIZATION
// ------------------------------------
const optimization = {
optimization: {
splitChunks: {
chunks: ‘all‘,
minChunks: 2
},
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
compress: {
unused: true,
dead_code: true,
warnings: false
}
},
sourceMap: true
}),
new OptimizeCSSAssetsPlugin({})
]
},
performance: {
hints: false
}
};
我们可以看到,UglifyJsPlugin 主要用于减小 App 的代码体积,而 OptimizeCSSAssetsPlugin 用于减小样式文件的大小。
阶段插件注入
这是第三步——基于模式(test/dev/prod)进行插件注入。每个模式都有自己的注入插件套件,以不同的方式运行 App:
production——运行应用了所有优化的 App,让代码变得更轻量级和稳定。
// ------------------------------------
// STAGE PLUGINS INJECTION! [DEVELOPMENT, PRODUCTION, TESTING]
// ------------------------------------
const stagePlugins = {
test: [new BundleAnalyzerPlugin()],
development: [
new HtmlWebpackPlugin({
template: path.resolve(‘./src/index.html‘),
filename: ‘index.html‘,
inject: ‘body‘,
minify: false,
chunksSortMode: ‘auto‘
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
],
production: [
new MiniCssExtractPlugin({
filename: ‘[name].[hash].css‘,
chunkFilename: ‘[name].[hash].css‘
}),
new HtmlWebpackPlugin({
template: path.resolve(‘./src/index.html‘),
filename: ‘index.html‘,
inject: ‘body‘,
minify: {
collapseWhitespace: true
},
chunksSortMode: ‘auto‘
})
]
};
先让我们暂停一会儿,讨论一下上面的这些插件。
BundleAnalyzerPlugin——我们只在 test 模式下使用它。它以可视化的方式向我们提供有关 App 包大小的信息,当你的 App 规模变大时,它是一个非常有用的工具。
HtmlWebpackPlugin——帮助我们处理常规 html 文件中的代码插入。在编写代码时,我们需要手动完成这些插入。
HotModuleReplacementPlugin——它通过监听文件变化并自动加载被修改的文件来减少烦人的浏览器手动重新加载。
NoEmitOnErrorsPlugin——它是一个小插件,有助于减少来自 CLI 的烦人的无用警告消息。
MiniCssExtractPlugin——有助于在生产构建期间优化样式文件大小。
最后一个配置
现在到了 Webpack 配置的最后一点。在导出配置之前,需要将之前讨论的所有内容结合起来。
const createConfig = () => {
debug(‘Creating configuration.‘);
debug(`Enabling devtools for ‘${__NODE_ENV__} Mode!‘`);
const webpackConfig = {
mode: __DEV__ ? ‘development‘ : ‘production‘,
name: ‘client‘,
target: ‘web‘,
devtool: stageConfig[__NODE_ENV__].devtool,
stats: stageConfig[__NODE_ENV__].stats,
module: {
rules: [...rules]
},
...optimization,
resolve: {
modules: [‘node_modules‘],
extensions: [‘.ts‘, ‘.tsx‘, ‘.js‘, ‘.jsx‘, ‘.json‘]
}
};
// ------------------------------------
// Entry Points
// ------------------------------------
webpackConfig.entry = {
app: [‘babel-polyfill‘, path.resolve(__dirname, ‘src/index.js‘)].concat(
‘webpack-hot-middleware/client?path=/__webpack_hmr‘
)
};
// ------------------------------------
// Bundle externals
// ------------------------------------
webpackConfig.externals = {
react: ‘React‘,
‘react-dom‘: ‘ReactDOM‘
};
// ------------------------------------
// Bundle Output
// ------------------------------------
webpackConfig.output = {
filename: ‘[name].[hash].js‘,
chunkFilename: ‘[name].[hash].js‘,
path: path.resolve(__dirname, ‘dist‘),
publicPath: ‘/‘
};
// ------------------------------------
// Plugins
// ------------------------------------
debug(`Enable plugins for ‘${__NODE_ENV__} Mode!‘`);
webpackConfig.plugins = [
new webpack.DefinePlugin({
__DEV__,
__PROD__,
__TEST__
}),
...stagePlugins[__NODE_ENV__]
];
// ------------------------------------
// Finishing the Webpack configuration!
// ------------------------------------
debug(`Webpack Bundles is Ready for ‘${__NODE_ENV__} Mode!‘`);
return webpackConfig;
};
module.exports = createConfig();
上述代码有三个部分值得我们关注:
webpackConfig.entry——告诉 webpack 应该从哪里开始处理 App 代码,我们在这里包含了一些奇怪的代码,这样可以直接在 App 中使用 webpack HMR。
webpackConfig.externals——这个部分可以帮助我们完全排除 react 包,我们将在 index.html 文件中从 CDN 获取 react 包。这样可以让我们的捆绑包更轻量级。
webpackConfig.output——告诉我们如何获得输出的 App 代码。通常代码块应该是:chunk_name.hash_code.js。
现在,我们得到了一个完整的 webpack.config.js 配置文件,它可以以各种方式处理我们的 App——从测试到生产模式!
## 2. 服务器配置
现在,我们可以继续开始配置我们的本地服务器。
我们有几种方法可用来创建 App 服务器:
* 使用 NodeJS + Express 从头开始编写自己的服务器(这种方式要求你具备 JS 后端技术栈方面的专业知识);
* 使用一些传统的第三方软件包;
* 采用流行的 BrowserSync 插件(https://www.browsersync.io/),用来快速构建自定义服务器。
我们将使用最后一个选项,所以需要先为 BrowserSync 添加几个包:
yarn add browser-sync connect-history-api-fallback path
现在我们已经准备好要创建 /server/ 文件夹,根目录中包含两个文件:server.js(主服务器文件)和 compiler.js(包含 App 编译过程的响应信息)。
与 webpack 配置一样,我们也需要在 server.js 文件的顶部添加一些以前安装的软件包:
// import global vars for a whole app
require(‘../globals‘);
const path = require(‘path‘);
const browserSync = require(‘browser-sync‘);
const historyApiFallback = require(‘connect-history-api-fallback‘);
const webpack = require(‘webpack‘);
const webpackDevMiddleware = require(‘webpack-dev-middleware‘);
const webpackHotMiddleware = require(‘webpack-hot-middleware‘);
const webpackConfig = require(‘../webpack.config.js‘);
const bundler = webpack(webpackConfig);
在这里,我们看到了一些有趣的包。
* webpackDevMiddleware 和 webpackHotMiddleware——它们是甜蜜的一对,可以帮助我们在开发过程中使用热模块替换(HMR);
* historyApiFallback——在导览应用页面期间更新浏览器历史记录;
* path——用于处理 App 项目文件的路径。
现在我们准备开始编写服务器配置了。首先,我们需要从 HMR 配置开始:
// ========================================================
// WEBPACK MIDDLEWARE CONFIGURATION
// ========================================================
const devMiddlewareOptions = {
publicPath: webpackConfig.output.publicPath,
hot: true,
headers: { ‘Access-Control-Allow-Origin‘: ‘*‘ }
};
下一步是服务器配置的要点,我们将进行 browserSync 的服务器配置:
// ========================================================
// Server Configuration
// ========================================================
browserSync({
open: false,
ghostMode: {
clicks: false,
forms: false,
scroll: true
},
server: {
baseDir: path.resolve(__dirname, ‘../src‘),
middleware: [
historyApiFallback(),
webpackDevMiddleware(bundler, devMiddlewareOptions),
webpackHotMiddleware(bundler)
]
},
files: [
‘src/../.tsx‘,
‘src/../.ts‘,
‘src/../.jsx‘,
‘src/../.js‘,
‘src/../.json‘,
‘src/../.scss‘,
‘src/../*.html‘
]
});
这里有两个重要的属性值得我们关注:
* server——我们可以以各种方式调整服务器。在我们的例子中,我们需要通过 baseDir 属性来注入 App 的入口,并通过注入 webpackDevMiddleware 和 webpackHotMiddleware 来启用 HMR。
* files——也是一个很重要的属性,因为它将监听项目中的所有文件类型。不要漏掉了任何需要重新加载的文件类型!
这是它在 CLI 中的样子:
![](https://s4.51cto.com/images/blog/202012/19/74f7ef8461e6f4e7bcfc1c2cf31ac233.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
完成了服务器配置,现在,我们可以开始我们的 App 开发了。但是等一下,只是完成服务器配置就可以编译项目了吗?我们还需要创建编译器配置!
所以,让我们在 /server/ 文件夹中创建 compier.js 文件。从导入包开始:
// import global vars for a whole app
require(‘../globals‘);
const debug = require(‘debug‘)(‘app:build:webpack-compiler‘);
const webpack = require(‘webpack‘);
const webpackConfig = require(‘../webpack.config.js‘);
我们看到了一个新的 debug 包——它会在 CLI 中输出任何你想输出的信息。在自定义服务器调试是它会非常有用,我非常喜欢它。
yarn add debug
现在我们可以开始进行编译器配置:
// -----------------------------
// READING WEBPACK CONFIGURATION
// -----------------------------
function webpackCompiler() {
return new Promise((resolve, reject) => {
const compiler = webpack(webpackConfig);
compiler.run((err, stats) => {
if (err) {
debug(‘Webpack compiler encountered a fatal error.‘, err);
return reject(err);
}
const jsonStats = stats.toJson();
debug(‘Webpack compilation is completed.‘);
resolve(jsonStats);
});
});
}
在上面的配置中,我们有一个常规函数 webpackCompiler,它就像一个编译过程的处理程序。它使用我们之前创建的 webpack 包和 webpack.config.js 文件。在处理结束时,如果成功编译了整个 App 代码,它将返回一个基于 Promise 的响应。
现在我们需要使用这个函数,让我们来创建另一个叫作 compile 的包装器:
// -----------------------------
// STARTING APP COMPILATION PROCESS
// -----------------------------
const compile = () => {
debug(‘Starting compiler.‘);
return Promise.resolve()
.then(() => webpackCompiler())
.then(() => {
debug(‘Compilation completed successfully.‘);
})
.catch(err => {
debug(‘Compiler encountered an error.‘, err);
process.exit(1);
});
};
compile();
在 CLI 中我们可以看到:
![](https://s4.51cto.com/images/blog/202012/19/f2fbd234f02685e5fadef15c0fb8268b.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
我们已经完成了服务器和编译器的配置!
## 3. 客户端配置
在完成服务器端的配置之后,现在,我们必须考虑如何处理 App 的客户端。正如我们在开始时所说的那样,我们的抽象 App 应该具备创建多页面、进行代码拆分和发出 AJAX 请求的能力。
因为我们是基于 React 技术栈,所以将使用下一个库组合——React + Redux + Router + Saga。但是,首选需要为它们安装所有必需的包:
yarn add react react-dom redbox-react redux react-router react-router-dom react-router-redux connetced-react-router redux-saga redux-logger redux-saga-watch-actions
假设我们知道我们将在客户端使用哪些东西,现在我们需要想想如何将所有这些库绑定在一起。首先,它们需要主入口点和控制器配置。为了更清楚地说明,我们将它拆解为几个部分:
* App 入口点——它是客户端的核心,我们将其放入 webpack,用以处理整个 App 代码;
* Redux 配置——客户端 App 数据存储;
* 路由器配置——帮助我们创建多页 App 的路由环境;
* Saga 配置——异步请求的 App 环境,将响应 AJAX 的数据更新,等等。
现在我们已经准备好配置客户端。我们先在 /src/ 文件夹中创建主 App 入口点 index.js 文件。
** App 入口点**
import React from ‘react‘;
import ReactDOM from ‘react-dom‘;
import RedBox from ‘redbox-react‘;
import store from ‘./controller/store‘;
import history from ‘./controller/history‘;
import AppContainer from ‘./containers/AppContainer‘;
const ENTRY_POINT = document.querySelector(‘#react-app-root‘);
// creating starting endpoint for app.
const render = () => {
ReactDOM.render(<AppContainer store={store} history={history} />, ENTRY_POINT);
};
// this will help us understand where the problem is located once app will fall.
const renderError = error => {
ReactDOM.render(<RedBox error={error} />, ENTRY_POINT);
};
// register serviceWorkers if available
if (‘serviceWorker‘ in navigator) {
navigator.serviceWorker
.register(‘./serviceWorker.js‘)
.then(registration => {
console.log(‘Excellent, registered with scope: ‘, registration.scope);
})
.catch(e => console.error(‘ERROR IN SERVICE WORKERS: ‘, e));
}
// This code is excluded from production bundle
if (DEV) {
// ========================================================
// DEVELOPMENT STAGE! HOT MODULE REPLACE ACTIVATION!
// ========================================================
const devRender = () => {
if (module.hot) {
module.hot.accept(‘./containers/AppContainer‘, () => render());
}
render();
};
// Wrap render in try/catch
try {
devRender();
} catch (error) {
console.error(error);
renderError(error);
}
} else {
// ========================================================
// PRODUCTION GO!
// ========================================================
render();
}
我认为以上所有内容都很容易理解,有几个地方需要再着重解释一下:
* redbox-react——如果出现了错误,它会将错误抛到浏览器窗口中;
* serviceWorker——这个与 PWA 有关,我们稍后会讨论它,现在只需要记得我们是在什么地方注入它的;
* module.hot——与 HMR 有关,如果在开发期间更改了某些项目资源,它允许我们重新加载它们;
现在,我们为 App 客户端提供了一个随时可用的配置,剩下的是创建入口点涉及的所有必需的目录和文件:
* /src/controller/store.js,/src/controller/history.js
* /src/containers/AppContainer.js
** Redux 存储配置**
下一个重要步骤是创建用于收集客户端 App 数据的根存储配置。在我们的例子中,我们将使用 Redux。我们需要在 /src/controller/ 目录中创建 store.js 文件,看起来如下所示:
import { applyMiddleware, compose, createStore } from ‘redux‘;
import { routerMiddleware } from ‘connected-react-router‘;
import initialState from ‘./initialState‘;
import history from ‘./history‘;
import { logger, makeRootReducer, sagaMiddleware as saga, rootSaga, runSaga } from ‘./middleware‘;
// creating the root store config
const rootStore = () => {
const middleware = [];
// Adding app routing
middleware.push(routerMiddleware(history));
// Adding async Saga actions environment
middleware.push(saga);
// Adding console logger for Redux
middleware.push(logger);
const enhancers = [];
// allow to use the redux browser plugin
if (DEV && window.REDUX_DEVTOOLS_EXTENSION) {
enhancers.push(window.REDUX_DEVTOOLS_EXTENSION());
}
// ======================================================
// Store Instantiation
// ======================================================
const store = createStore(
makeRootReducer(),
initialState,
compose(
applyMiddleware(...middleware),
...enhancers
)
);
// saga injecting during code-splitting
store.runSaga = runSaga;
runSaga(rootSaga);
store.asyncReducers = {};
return store;
};
export default rootStore();
这是 App 业务逻辑的要点。在这个配置文件中,我们绑定了所有环境,在后续会用到。这里需要着重解释一些东西:
rootStore 函数对存储进行了包装,我们在其中设置了常规的 Redux,并通过第三方中间件对它进行扩展:
* connected-react-router——有助于我们设置 App 路由器;
* redux-logger——可以通过浏览器控制台监控动作分派;
* redux-saga——有助于处理基于 Redux 存储的 AJAX 请求;
* redux-saga-watch-actions——有助于根据加载的捆绑包注入 saga(只有在进行代码拆分时才需要这个)。
在这里我想先告诉你一些关于代码拆分的东西。我们已经在 Webpack 中配置了它,但也需要为 Redux 存储做好准备。我是这么做的:
/src/store/middleware/rootReducer.js
...
// Code-Splitting environment
export const injectReducer = (store, { key, reducer }) => {
if (Object.hasOwnProperty.call(store.asyncReducers, key)) return;
store.asyncReducers[key] = reducer;
store.replaceReducer(makeRootReducer(store.asyncReducers));
};
injectReducer 是一个常规的 JS 函数表达式,每次更新 App 存储时都会执行检查。我们假设我们已经准备好了一个多页 App,当用户点击下一页,新页面的 reducer 将被附加到根 Redux 存储,在加载块时不会产生任何副作用。
** Saga 配置**
这是客户端控制器逻辑的一个非常有趣的部分——AJAX Saga 动作。我不打算说明它的工作原理,你可以参考它的官方网站:https://redux-saga.js.org/
我将向你展示如何在常规任务中使用它,比如获取一些响应,等等。你可以在下面看到它的工作原理:
// saga entry point - propbably you don‘t need this
import createSagaMiddleware from ‘redux-saga‘;
import { all } from ‘redux-saga/effects‘;
import createSagaMiddlewareHelpers from ‘redux-saga-watch-actions/lib/middleware‘;
import watchSagas from ‘../../modules/saga‘;
const sagaMiddleware = createSagaMiddleware();
const runSaga = saga => sagaMiddleware.run(saga);
const { injectSaga, cancelTask } = createSagaMiddlewareHelpers(sagaMiddleware); // <-- bind to sagaMiddleware.run
export function* rootSaga() {
yield all([watchSagas()]);
}
export { cancelTask, injectSaga, runSaga };
export default sagaMiddleware;
rootSaga.js 文件必须放在 /src/controller/middleware/ 文件夹中。它包含了用于监听核心函数 rootSaga() 中异步操作的 Saga 逻辑。此外,我们在代码拆分模式下使用额外的 Saga 第三方库 redux-saga-watch-actions。如果有必要,它可以帮助我们动态创建和删除 Saga。
在我们的示例中,我们将使用来自 /src/modules/saga/someSaga.js 的函数 someSaga 作为 Saga 动作:
import { put } from ‘redux-saga/effects‘;
import { someAsyncAction } from ‘../actions‘;
export function* someSaga() {
try {
const payload = yield fetch(‘https://www.github.com‘);
// throw an error if no payload received
if (!payload) {
throw new Error(‘Error in payload!‘);
}
// some payload from the responce received
yield put(someAsyncAction(payload));
} catch (error) {
throw new Error(‘Some error in sagas occured!‘);
}
}
export default someSaga;
我们可以看到,someAsyncAction() 是一个简单的 Redux 动作,在异步 AJAX Saga 获取请求完成时就会被触发,并将从服务器获取到的有效载荷放在 Redux 存储中。
** 路由器配置**
现在我们只需要配置抽象 App 的路由。在我们的例子中,我们有意要用到路由!
import { PropTypes } from ‘prop-types‘;
import React from ‘react‘;
import { Provider } from ‘react-redux‘;
import { ConnectedRouter } from ‘connected-react-router‘;
import CoreLayout from ‘../layout‘;
const AppContainer = ({ store, history }) => {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<CoreLayout />
</ConnectedRouter>
</Provider>
);
};
AppContainer.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
export default AppContainer
```;
这是 App 组件的主要容器。这个很有意思,因为它包含整个路由逻辑。我们在这里使用 connected-react-router 库来处理 React-Redux 模式中的导航。
关于 CoreLayout 组件:
import React from ‘react‘;
import { Route, withRouter } from ‘react-router-dom‘;
import { Header, Footer } from ‘../components‘;
import { Body } from ‘../containers/Wrappers‘;
import styles from ‘../styles/index.scss‘;
const CoreLayout = () => {
return (
<div className={styles.appWrapper}>
<Header />
<Route exact path=‘/‘ component={Body} />
<Footer />
</div>
);
};
export default withRouter(CoreLayout);
这是 App 的主要布局点,显示了路由是如何被注入到代码中的。基本上,这个例子涵盖了 Web 开发中的常见情况。
我想强调一下,我们已经在 /src/controller/ 目录的 store.js 和 rootReducer.js 文件中配置了 App 路由,所以在这里我们只需要像普通的 React 组件一样使用它,把 App 组件包装在其中——一个带有 history 对象的组件。
现在,如果你启动 App,就可以看到路由可以正常工作,而且 Redux 存储可以做出响应!redux-logger 库可以帮我们看到这些。此外,HMR 也被触发了:
现在,我们已经完成了路由和 App 客户端的配置!
在结束之前,我们还需要做一件大事——写几个有趣的脚本来配置我们的 RSK Builder。
Babel 配置
因为 Babel 7 支持 TypeScript,还提供了更新的库组件,所在在配置时需要注意。
首先是安装所有必需的包:
yarn add @babel/cli @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread @babel/plugin-syntax-dynamic-import @babel/plugin-transform-async-to-generator @babel/plugin-transform-modules-commonjs @babel/plugin-transform-object-assign @babel/plugin-transform-runtime @babel/polyfill @babel/preset-env @babel/preset-react @babel/preset-stage-3 @babel/preset-typescript awesome-typescript-loader babel-core babel-jest babel-loader babel-plugin-transform-runtime babel-polyfill
我们从根文件夹中的.babelrc 主配置文件开始:
{
"presets": [
"@babel/env",
"@babel/react",
"@babel/typescript"
],
"plugins": [
"@babel/plugin-transform-object-assign",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-syntax-dynamic-import"
]
}
我们可以看到:
{
test: /\.(js|jsx|ts|tsx)?$/,
exclude: /(node_modules|bower_components)/,
loader: ‘babel-loader‘
}
到这里,Babel 就配置好了。
这个要再次感谢 Babel 7,因为现在我们完全支持 TypeScript 预处理器!但在开始之前,需要先添加它:
yarn add typescript
我们必须在根目录的 TypeScript 配置文件 tsconfig.json 中配置如下内容:
{
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"checkJs": false,
"module": "esnext",
"target": "es5",
"jsx": "react",
"moduleResolution": "node",
"lib": ["es6", "dom"],
"outDir": "./dist",
"rootDir": "./src"
},
"typeAcquisition": {
"enable": true
},
"typeRoots": [
"./typings.d.ts",
"./node_modules/@types"
],
"include": [
".src/.ts",
"src/**/*",
"./typings.d.ts"
],
"exclude": [
"node_modules",
"**/*.test.ts",
"server",
"dist"
]
}
以上这些只是一个普通的 JSON 对象,你需要知道几个属性:
yarn add jest jest-cli ts-jest babel-jest enzyme enzyme-adapter-react-16 enzyme-to-json
然后,我们可以开始在根目录创建 jest.config.json 文件。它看起来像这样:
module.exports = {
cacheDirectory: ‘<rootDir>/.tmp/jest‘,
coverageDirectory: ‘./.tmp/coverage‘,
moduleNameMapper: {
‘^.+\\.(css|scss|cssmodule)$‘: ‘identity-obj-proxy‘
},
modulePaths: [‘<rootDir>‘],
moduleFileExtensions: [‘ts‘, ‘tsx‘, ‘js‘, ‘jsx‘, ‘json‘],
globals: {
NODE_ENV: ‘test‘
},
verbose: true,
testRegex: ‘(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js|jsx)$‘,
testPathIgnorePatterns: [‘/node_modules/‘, ‘/__tests__/mocks/.*‘],
coveragePathIgnorePatterns: [‘typings.d.ts‘],
transformIgnorePatterns: [‘.*(node_modules).*$‘],
transform: {
‘^.+\\.jsx?$‘: ‘babel-jest‘,
‘^.+\\.tsx?$‘: ‘ts-jest‘
},
setupFiles: [‘<rootDir>/setupTests.js‘],
snapshotSerializers: [‘enzyme-to-json/serializer‘]
};
在这里,我只描述你需要注意的几个属性:
// TODO: Remove these polyfills once the below issue is solved.
// It present here to allow Jest work with the last React environment.
// https://github.com/facebookincubator/create-react-app/issues/3199#issuecomment-332842582
global.requestAnimationFrame = cb => {
setTimeout(cb, 0);
};
global.matchMedia = window.matchMedia || function() {
return {
matches: false,
addListener: () => {},
removeListener: () => {}
};
};
import Enzyme from ‘enzyme‘;
import Adapter from ‘enzyme-adapter-react-16‘;
Enzyme.configure({ adapter: new Adapter() });
由于 Jest/Enzyme 不完全支持当前版本的 React 环境,我们需要在根目录创建上述的 setupTests.js 文件来使它们能够协同工作。我们还配置了一个 Enzyme-Jest Adapter,将这对甜蜜的情侣结合在一起。
TS/JS liner 配置
我总是会在项目中为所有代码和样式文件使用准备好的 lint:
yarn add husky lint-staged
然后在我们的 App 根目录的 package.json 文件中配置它们:
...
...
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.json": [
"jsonlint --formatter=verbose",
"git add"
],
"*.@(css|scss)": [
"stylelint --fix --formatter=verbose",
"git add"
],
"*.cssmodule": [
"stylelint --fix --syntax scss --formatter=verbose",
"git add"
],
"*.@(js|jsx)": [
"prettier --write",
"eslint --fix --quiet",
"git add",
"jest --bail --findRelatedTests"
],
"*.@(ts|tsx)": [
"prettier --write --parser typescript",
"tslint --fix -c tslint.json",
"git add",
"jest --bail --findRelatedTests"
]
},
...
...
...
当我们在 CLI 中运行 git commit 命令时,husky 库会捕获到这个命令并暂停提交过程,进行预提交检查。第二个库 lint-staged 开始执行代码检查。
在 lint-staged 配置中,我们设置了文件检查规则:
module.exports = {
plugins: {
‘postcss-import‘: {},
‘postcss-cssnext‘: {},
‘postcss-preset-env‘: {},
‘cssnano‘: {}
}
};
module.exports = {
useTabs: false,
printWidth: 120,
tabWidth: 2,
singleQuote: true,
trailingComma: ‘none‘,
jsxBracketSameLine: false,
semi: false
};
脚本运行配置
我们已经做好了最后的准备——将所有的工作结合在一起,只需输入一个命令即可运行我们的 App。
我们需要再次打开 package.json,并添加几个脚本规则。但在此之前,我们需要先添加一些包:
yarn add better-scripts
现在我们可以继续编写脚本:
...
...
...
"scripts": {
"start:dev": "better-npm-run start:dev",
"start:prod": "better-npm-run start:prod",
"test": "better-npm-run test",
"clean": "rimraf dist",
"push": "npm run lint && git push",
"compile": "better-npm-run compile",
"tslint": "tslint --fix -c tslint.json",
"eslint": "eslint --quiet ../../.eslintrc",
"csslint": "stylelint **/*.scss --config ../../.stylelintrc"
},
"betterScripts": {
"compile": {
"command": "node server/compiler",
"env": {
"NODE_ENV": "production",
"DEBUG": "app:*"
}
},
"start:dev": {
"command": "node server/server",
"env": {
"NODE_ENV": "development",
"DEBUG": "app:*"
}
},
"start:prod": {
"command": "node server/server",
"env": {
"NODE_ENV": "production",
"DEBUG": "app:*"
}
},
"test": {
"command": "node server/server",
"env": {
"NODE_ENV": "test",
"DEBUG": "app:*"
}
}
},
"repository": {
"type": "git"
},
...
...
...
在这里你可以看到一些非常规的属性——betterScripts。我喜欢这个库,因为我们不仅可以运行脚本本身,还可以设置一些额外的属性。例如,可以选择在哪个模式(dev/prod/test)下运行 App,以及其他更多的东西。
让我们来看看上面的配置。当我们启动其中一个命令时,它将运行相应的脚本集:
英文原文:
https://medium.com/@svyat770/lets-kill-create-react-app-452cb55f77d3
扔掉Create React App,打造你自己的React生成工具!
标签:rail 更新 glob during 动画 smo mesa fine jpg
原文地址:https://blog.51cto.com/15057848/2567786