作者|Sviat Kuzhelev
译者|无明
每个人都喜欢现成的东西. 很显然, 对于基于 React 的代码生成系统来说, 没有什么比 Facebook 团队推出的 create-react-App 更好的了. 是的, 它非常有用. 有了它, 你可以立马开始 App 的编码工作. 但从另一面来看, 这种方式也让我们失去了了解内部工作原理的机会. 我们应该要透过美丽的高级 API, 了解它的内在机制. 所以, 今天就让我们来尝试构建自己的第一个 React Starter Kit Builder(RSK)!
在这篇文章中, 我将使用我的全新 RSK Builder, 你可以从 GitHub 上克隆:
https://github.com/BiosBoy/coconat
从头开始
让我们回到最初, 从我们的想象开始. 我们什么都没有, 只有笔记本电脑和我们已经掌握的牢固的 JS 知识. 为了增加趣味, 我们也可以说我们需要创建一个抽象的真实世界的商业 App, 我们需要实现很多现代功能, 例如:
基于 React 的开发;
严格的值类型;
支持旧版浏览器;
代码拆分性能优化;
与服务器进行 Ajax 通信;
Service Workers 数据更新线程;
美丽的动画布局;
这个时候你要问自己:"我应该从哪里开始"? 正确答案似乎是:"当然是 webpack!". 在今天, 很难想象哪个项目可以不使用 Webpack, 即便是我们最喜欢的 create-react-App 也在使用它.
因此, 首先要安装 Webpack. 我们需要为未来的 App 创建一些工作文件夹, 假设是 "CoConat".
在这篇文章中, 我们将使用典型的文件夹结构:
下一步是通过 yarn/NPM 初始化 App:
yarn init
在这一步, CLI 中有一些繁琐的字段需要填写. 假设你已经填好了, 现在, 和往常一样, 我们得到了一个 App 初始化文件夹. 我们可以开始安装所有必需的 NPM 包和 Webpack.
yarn add webpack
1. 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 个主要配置项:
文件加载器规则(JS/CSS/JSON): 处理 App 中使用的所有文件, 并控制如何在输出时解释它们.
捆绑包优化: 一组 Webpack 环境模式, 可帮助我们对应用程序代码进行调优, 例如添加源映射, 进行代码分割, 等等.
阶段插件注入(开发 / 生产): 一个强大的工具, 根据 dev/prod/test 模式在所有编译过程中分离代码.
在 webpack.config.JS 文件的顶部, 我们需要添加一些以前安装的包:
// import global vars for a whole apprequire('./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' }];
这里有一些有趣的东西:
我们有 JS/jsx/ts/tsx 文件的预加载器. 我们可以使用一些特殊的工具 (比如 JS/ts-linters) 来预先处理代码中的错误, 在它们破坏我们的 App 之前捕获它们.
我们有 JS/jsx/ts/tsx/sccs/HTML/JSON 和其他常规文件加载器. 有了这些加载器, webpack 可以通过各种方式处理我们的 App. 所以, 我们可以看到配置中有样式加载器部分, 其中三元表达式规定了如何加载样式文件: 在生产阶段使用 MiniCssExtractPlugin 插件, 在开发阶段使用自定义样式加载器, 这样可以让我们的代码更加扁平化.
捆绑包优化
webpack 配置中的下一个重要步骤是清楚地了解如何有效地优化 App 代码的输出. 好在这不是很难, 我们只需填写三个重要的属性:
optimization-- 有助于我们在未来进行代码拆分 ;
minimizer-- 可以极大地减小输出文件的体积, 最多可以减少高达 70%的大小;
perfomance-- 一个额外的部分, 可以包含开发过程中的一些开发特性和一些次要的东西.
// ------------------------------------// 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:
test-- 在运行 App 时带有捆绑包权重检查, 可以帮助我们找到 App 中最大或未使用的部分;
development-- 以一种常见的方式运行 App, 只需进行最少的优化或改进, 例如在 index.HTML 文件正文中插入样式文件或添加热模块替换, 等等;
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 服务器:
使用 Node.JS + 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 apprequire('../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 中的样子:
完成了服务器配置, 现在, 我们可以开始我们的 App 开发了. 但是等一下, 只是完成服务器配置就可以编译项目了吗? 我们还需要创建编译器配置!
所以, 让我们在 /server/ 文件夹中创建 compier.JS 文件. 从导入包开始:
// import global vars for a whole apprequire('../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 中我们可以看到:
我们已经完成了服务器和编译器的配置!
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 availableif ('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 bundleif (__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 configconst 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__ && Windows.__REDUX_DEVTOOLS_EXTENSION__) { enhancers.push(Windows.__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 environmentexport 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 thisimport 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.runexport 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 客户端的配置!
4. 核心配置
在结束之前, 我们还需要做一件大事 -- 写几个有趣的脚本来配置我们的 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" ]}
我们可以看到:
presets-- 处理非常见的 JS 语法, 并将代码转换为 ES5 常规语法. 在我们的例子中, 它们是 React 和 TypeScript;
plugins-- 有助于使用自定义语法, 方法, 函数, 例如新的 ECMAScript 特性, 可以早于 TC39 使用这些特性. 在这里我只包括了三个最有用的插件, 用于日常开发.
至于 Webpack + Babel 的绑定, 我们已经在文章第一部分的 webpack.config.JS 规则中配置好了:
{ test: /\.(JS|jsx|ts|tsx)?$/, exclude: /(node_modules|bower_components)/, loader: 'babel-loader'}
到这里, Babel 就配置好了.
TypeScript 配置
这个要再次感谢 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 对象, 你需要知道几个属性:
compilerOptions-- 是 TypeScript 配置的核心. 在这里, 我们可以根据需要调整 TS 编译器. 但对于大多数情况, 目前的配置就可以了;
include-- 表示 TypeScript 应该从哪里开始运行, 在我们的例子中是 /src/ 文件夹;
exclude-- 我们可以设置需要被排除在编译过程之外的文件夹列表.
在配置好这个文件后, Webpack 将在编译过程中自动选择它. 另外, 不要忘记在跟目录创建 typings.d.ts 文件 (因为文件太长了, 这里只给出文件链接: https://gist.github.com/BiosBoy/e4ec11fc02b7421daadd70b1deee8ba2) 它将有助于处理 CSS 模块和全局变量.
Jest 配置
到这里, 我们几乎完成了我们的 RSK Builder 之旅! 我们现在还需要做几件事情, 其中之一就是搭建测试环境.
在自动化测试工具之战中, 最大的赢家是 Mocha 和 Jest. 我们选择了后者 --Jest, 还使用了一个额外的 DOM 测试库 --Enzyme.
先安装它们:
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']};
在这里, 我只描述你需要注意的几个属性:
testRegex-- 有助于在整个 App 中查找测试文件;
transform-- 在测试运行期间将原始代码转换为 ES5;
snapshotSerializers-- 为运行的每个测试文件创建快照.
这个配置表示测试文件可以是. JS/.jsx 和. ts/.tsx, 可以把 CSS 模块和快照包含在其中.
但这并不是全部!
// 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-332842582global.requestAnimationFrame = cb => { setTimeout(cb, 0);};global.matchMedia = Windows.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:
ESLint 配置:
https://gist.github.com/BiosBoy/118dd6bef60b36d6a1034f10f83ffe45
TSLint 配置:
https://gist.github.com/BiosBoy/9e9c9f7e6095384c86567bfda2e2724c
StyleLint 配置:
https://gist.github.com/BiosBoy/79a4b6a5a52fc2e30cb9c0fba0c6ee17
编辑器配置:
https://gist.github.com/BiosBoy/46a3dc16276362d813b0c2e7b67410c2
预提交 hook 配置
现在我们差不多完成了 RSK Builder 的创建.
除了代码样式检查之外, 遵循版本控制系统 (Git) 的规则也很重要. 很多初学者都会犯同样的错误 -- 他们没有在 Git 预提交 hook 上预先配置代码检查!
我们将创建并实现自定义预提交 hook, 用来检查 JSON/JS/jsx/ts/tsx/SCSS 代码中的错误. 为此, 我们需要安装几个有用的包:
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 配置中, 我们设置了文件检查规则:
通过 prettier 来美化代码;
通过 ts-lint/JS-lint/stylelint/jsonlint 来检查错误;
如果存在这些文件, 就运行 jest 测试.
另外, 不要忘记在根目录中创建另外两个文件: prettier.config.JS 和 postcss.config.JS. 它们不只是用于代码和样式检查:
- 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, 以及其他更多的东西.
让我们来看看上面的配置. 当我们启动其中一个命令时, 它将运行相应的脚本集:
start:dev: 在开发模式下启动 App;
start:prod: 在生产模式下启动 App;
test: 在测试模式下启动 App;
clean: 在 App 编译之前清理 /dist/ 文件夹;
push: 在 Git 服务器上部署新提交的代码;
compile: 将 App 代码编译到 /dist/ 文件夹中;
tslint: 启动 lint 检查当前. ts/tsx 文件;
eslint: 启动 lint 检查当前. JS/ssx 文件;
csslint: 启动 lint 检查当前. SCSS 文件.
从现在开始, 我们可以通过 CLI 中的一个命令来启动我们的 App, 然后就可以看好戏了!
来源: https://juejin.im/entry/5c490f2cf265da61273db397