基於 Webpack 3 的 React 工程項目腳手架

基於 Webpack 3 的 React 工程項目腳手架從屬於筆者的 Web 前端入門與工程實踐,算來已經是筆者 React 技術棧腳手架的第四個迭代版本。更多關於 React 或者前端開發相關的資料鏈接可以參考React 學習與實踐資料索引以及 Webpack 學習與資料索引,對於其中淺薄的工程化的思考可以參考 2016-我的前端之路:工具化與工程化。

基於 Webpack 3 的 React 工程項目腳手架

create-react-boilerplate 是筆者對於日常工作中的基於 React 技術棧與實踐的沉澱,dev-config/* 與 package.json 構成了基礎的腳手架,支持最新的開發流程與默認的生產環境優化;模板項目包含特性如下:

  • 技術棧支持:使用 ES6/ES7 語法、使用 React Router V4、允許使用 CSS Modules、SCSS、Less 並且使用 PostCSS 進行自動 Polyfill、支持使用 styled-component 進行 CSS-in-JS 樣式控制、使用 Flow 作為靜態類型檢測工具、使用 Jest 作為默認的測試框架
  • 開發環境:使用 WebpackDevServer 部署開發伺服器、使用 React Hot Loader 進行組件熱載入、使用 Babel 進行代碼轉換、使用 ESLint 進行代碼檢測、使用 DllPlugin 作為開發環境下公共代碼提取工具以優化編譯速度
  • 生產環境:使用 CommonChunksPlugin 作為生產環境下公共代碼提取工具、使用 Prepack & prepack-webpack-plugin 進行代碼優化、使用 offline-plugin 添加簡單的 PWA 特性增強
  • 部署方式:支持獨立部署(Hash 方式切換路由)、支持服務端部署、支持服務端渲染

我們可以直接拷貝該項目來展示部分開發模式或者作為模板項目使用:

# 下載本項目git clone https://github.com/wxyyxc1992/create-react-boilerplate# 可以使用 yarn install & npm start 直接運行本項目# 僅保留 dev-config、package.json、src/client.js、src/ssr_server.jsmkdir /path/to/your/project# 拷貝必須的啟動文件cp -r dev-config/ /path/to/your/projectcp package.json /path/to/your/projectcp src/client.js /path/to/your/project/src/cp src/ssr_server.js /path/to/your/project/src/# 安裝運行依賴cd /path/to/your/projectyarn install / npm install# 啟動項目npm start# 編譯為純客戶端部署模式,即單個 HTML 頁面npm run build# 編譯為服務端渲染模式(主要區別在於路由支持)npm run build:ssr# 進行依賴升級檢查npm run update# 啟動 Storybooknpm run storybook

此外本項目中的演示代碼還包含了性能優化、設計模式、樣式指南、Redux、MobX 等常見的開發模式,在線演示地址:wxyyxc1992.github.io/cr;目前演示代碼還處於完善階段,可以關注代碼倉庫了解最新更新:

  • 性能優化
    • 懶載入:
      • 組件的非同步載入:src/case/performance/lazy/loadable
      • 外部依賴腳本(JS / CSS)的非同步載入:src/case/performance/lazy/external/*
    • WebAssembly:WebAssembly 初體驗:重構計算模塊
      • 簡單計數器:src/case/performance/web_assembly/counter
      • WayOfLife 遊戲引擎:src/case/performance/web_assembly/game
  • 設計模式
    • 許可權校驗:
      • 基於 React-Router-V4 的登錄與許可權控制驗證:src/case/designpattern/auth
  • 樣式指南
  • Redux
  • MobX
    • TODOApp

未來筆者也會同步升級 create-react-boilerplate 命令行工具以快速創建項目;此外本文檔僅是對於項目中使用的 Webpack 配置進行說明,詳細的 Webpack 學習資料可以參考筆者在 React 與前端工程化實踐一書中的 React 初窺與 Webpack 工程實戰兩章。

基礎配置

create-react-boilerplate 默認的應用配置位於 dev-config/apps.config.js 文件中,該文件也是 dev-config/ 文件夾下唯一與應用業務相關的文件;該文件定義了不同應用中的需要配置的應用相關信息。create-react-boilerplate 定位為單項目多應用的模板,因此我們可以在apps 鍵下配置項目設計的應用入口;在打包時會自動將多個應用並行編譯並且提取出所有公共的代碼。每個應用需要提供唯一編號、入口文件地址、模板頁面、是否編譯等信息;接下來 devServer 則是定義了當前正在開發的應用入口,ssrServer 定義了打包時需要使用的渲染伺服器入口,其會在執行 npm run build:ssr 時調用,proxy 與 api 則定義了後端伺服器信息,開發者可以根據業務需求自行使用。典型的 apps.config.js 文件配置如下:

module.exports = { //基本的應用配置信息 apps: [ //HelloWorld { id: "pwa", src: "./pwa/client.js", indexPage: defaultIndexPage, compiled: true } ], //開發入口配置 devServer: { appEntrySrc: "./pwa/client.js", //當前待調試的APP的入口文件 port: 3000 //監聽的Server埠 }, //用於服務端渲染的Server路徑 ssrServer: { serverEntrySrc: "./pwa/ssr_server.js" }, //依賴項配置 proxy: { //後端伺服器地址 http://your.backend/ "/api/*": "http://localhost:3001" }, //後端 api 配置,這樣配置可以避免將測試伺服器埠暴露出去 api: { dev: {}, prod: {} }};

這裡還需要提及的是在 *client.js 入口文件中,我們還需要引入封裝之後的渲染方法以支持熱載入,其模板為:

// @flowimport React from "react";import App from "./container/App";import { clientRender } from "../dev-config/tool/render";//將組件渲染到DOM中clientRender(<App />, document.getElementById("root"), "./container/App", true);

腳本編譯與熱載入

在 dev-config/webpack/loaders.js 文件中定義了模板所需要的載入器,默認支持 js、jsx、ts、tsx、css、scss、less、json 以及各種資源文件等常見格式。當我們執行 npm start 命令時,會自動啟動dev-config/server/devServer.js 文件中定義的 Webpack 開發伺服器,該伺服器會使用 dev-config/webpack.config.js 文件進行配置項生成。值得一提的是,WebpackDevServer 中的 contentBase 設置為了 path.join(__dirname, "../../public"),也就是將 /public 目錄作為開發伺服器的默認根目錄。create-react-boilerplate 默認使用 react-hot-loader 添加 React 熱載入支持,其配置包括以下步驟:

  • 開發時應用入口設置:

entry = [ "react-hot-loader/patch", `webpack-dev-server/client?http://0.0.0.0:${appsConfig.devServer.port}`, "webpack/hot/only-dev-server", require("./apps.config.js").devServer.appEntrySrc ];

  • Babel 配置,默認的 Babel 文件位於 dev-config/tool/.babelrc:

... "plugins": [ "react-hot-loader/babel", ...

  • React Hot Loader 3 並未實現模塊熱替換介面,因此我們還需要重載自定義的渲染方法,參考 dev-config/tool/render.js 文件中的實現:

import React from "react"import ReactDOM from "react-dom"import { AppContainer } from "react-hot-loader"import App from "./containers/App"ReactDOM.render( <AppContainer> <App/> </AppContainer>, document.getElementById("root"));// Hot Module Replacement APIif (module.hot) { module.hot.accept("./containers/App", () => { const NextApp = require("./containers/App").default; ReactDOM.render( <AppContainer> <NextApp/> </AppContainer>, document.getElementById("root") ); });}

樣式處理

create-react-boilerplate 支持 SCSS、CSS Modules 以及 styled-components 這三種樣式定義方式,鑒於默認是將所有的 .css 文件按照 CSS Modules 方式載入;因此如果想不使用 CSS Modules 來聲明樣式,即使不使用 SCSS 語法也需要將樣式文件後綴聲明為 .scss。

  • SCSS

// 聲明.Showcase__container { height: 100%; ... // 左側導航欄 .Showcase__navigator { flex: 240px 0 0; ... } // 右側展示區域 .Showcase__cases { flex: 80% 1 1; padding: 5px 10px; }}// 引入import "./Showcase.scss";

  • styled-components

import styled from "styled-components";const ShowcaseHeaderContainer = styled.section` padding:1% 2%; margin-bottom:1%; background:white; color:rgba(0, 0, 0, 0.65); border-bottom:1px solid #e9e9e9; `;const ShowcaseHeaderTitle = styled.h1` color:rgba(0, 0, 0, 0.65);`;const ShowcaseHeaderDescription = styled.h2` color:rgba(0, 0, 0, 0.65);`;

  • CSS Modules

// 正常聲明.tip{ font-size: 20px;}// 使用import styles from "./Private.css";...<span className={styles.tip} />

在開發環境下樣式會被 style-loader 以內聯樣式導入,而生產環境下則會通過 ExtractTextPlugin 抽取為單獨的 CSS 文件。

exports.styles = { css: { test: /.css$/, use: __DEV__ ? ["style-loader", moduleCSSLoader, postCSSLoader] : ExtractTextPlugin.extract({ use: [moduleCSSLoader, postCSSLoader] }) }, scss: { test: /.(scss|sass)$/, use: __DEV__ ? ["style-loader", "css-loader", postCSSLoader, "sass-loader"] : ExtractTextPlugin.extract({ use: ["css-loader", postCSSLoader, "sass-loader"] }) }, less: { test: /.(less)$/, use: __DEV__ ? ["style-loader", "css-loader", postCSSLoader, "less-loader"] : ExtractTextPlugin.extract({ use: ["css-loader", postCSSLoader, "less-loader"] }) }};

Webpack 性能優化

公共代碼分割

create-react-boilerplate 使用了 CommonsChunkPlugin 進行代碼分割,默認在 dev-config/webpack/plugins.js 文件中定義了對於 node_modules 中依賴文件的自動抽取:

new webpack.optimize.CommonsChunkPlugin({ name: "vendor", filename: "vendor.bundle.js", minChunks: ({ resource }) => resource && resource.indexOf("node_modules") >= 0 && resource.match(/.(js|less|scss)$/) })

該插件會自動生成 vendor.bundle.js 文件,我們需要在應用入口文件之前引用它;開發者也可以自定義 CommonsChunkPlugin 插件以自定義需要提取的公共代碼。

構建性能優化

隨著項目複雜度與體量的增加,我們發現初始化編譯與增量編譯的速度都有所下降,為了提升構建性能首先我們要做的就是保持 Webpack 版本的更新速度;此外,create-react-boilerplate 還默認啟動了 DllPlugin 在開發狀態下將所有的依賴提取為 dll 文件以提高增量編譯的速度。因為考慮到靈活性,即隨時有可能增減依賴的情況,create-react-boilerplate 目前設置的是每次使用 npm start 的時候都會重新生成 dll 文件;如果是已經穩定的項目可以考慮僅生成一次依賴。

const path = require("path");const pkg = require("../package.json");const webpack = require("webpack");let dllConfig = { name: "vendor", entry: Object.keys(pkg.dependencies), output: { path: path.resolve(__dirname, "../public/dll"), filename: "vendor.bundle.js", library: "vendor_[hash]" }, plugins: [ new webpack.DllPlugin({ name: "vendor_[hash]", path: path.resolve(__dirname, "../public/dll/manifest.json") }) ]};module.exports = dllConfig;// 在 public/index.html 文件中需要引入該依賴// index.html<script src="dll/vendor.bundle.js"></script>

代碼編譯優化

create-react-boilerplate 中也內置了其他的編譯之後的代碼性能優化插件,首先是利用 Webpack 3 的 Scope Hositing 特性來優化生成的模塊;這一點需要使用 ModuleConcatenationPlugin 插件。此外,還使用了 PrepackWebpackPlugin 對於打包生成的文件進行過濾與重構;不過需要注意的是 PrepackWebpackPlugin 會較大地降低編譯速度,因此也是可以根據實際的項目情況選用。

// 使用 Scope Hositing 特性 new webpack.optimize.ModuleConcatenationPlugin(), // 使用 Prepack 優化包體大小 // 暫時存在 Bug,等待修復 // 使用前 21 - 425 // 使用後 21 - 433 new PrepackWebpackPlugin({ mathRandomSeed: "0" }),

PWA

create-react-boilerplate 中只是簡單地使用了 Offline Plugin,其配置如下:

// webpack.config.js examplevar OfflinePlugin = require("offline-plugin");module.exports = { // ... plugins: [ // ... other plugins // it"s always better if OfflinePlugin is the last plugin added new OfflinePlugin() ] // ...}// render.jsrequire("offline-plugin/runtime").install();

觀察網路面板中的資源請求情況,我們可以看到腳本等已經被緩存在了本地:

設計模式

組件非同步載入

在 create-react-boilerplate 中使用了 react-loadable 進行組件非同步分割與載入,參考 src/case/performance/lazy/Lazy.js 文件了解完整實現。我們首先通過 Loadable 封裝需要非同步載入的組件:

export const LoadableLazyComponent = Loadable({ loader: () => import("./LazyComponent"), loading: LoadingPlaceholder, delay: 200 // serverSideRequirePath: path.join(__dirname, "./LazyComponent"), // webpackRequireWeakId: () => require.resolveWeak("./LazyComponent")});

然後引入封裝組件 import { LoadableLazyComponent } from "./loadable/LoadableLazyComponent"; 如常使用即可。

服務端渲染

create-react-boilerplate 目前展示了基礎的基於 React Router V4 的服務端渲染支持:

// AppContainer.jsconst Router = __SSR__ ? BrowserRouter : HashRouter;// ssrServer.js//處理所有的請求地址app.get("/*", function(req, res) { try { // 判斷頁面是否匹配 const match = routes.reduce((acc, route) => { return matchPath(req.url, { path: route, exact: true }) || acc; }, false); // 如果待尋找頁面不存在 // 僅當訪問 404 界面時,提示不存在 if (match) { res.status(404).send(renderToString(<NoMatch location={req.url} />)); return; } // 存放渲染之後的 Context 數據 let context = {}; // 將組件渲染為 HTML let markup = renderToString( <StaticRouter context={context} location={req.url}> <App serverSideMessage={"Hello World By Server Side Rendering"} /> </StaticRouter> ); // 判斷是否存在轉發 if (context.url) { res.writeHead(301, { Location: context.url }); res.end(); } else { res .status(200) .send( renderHTML( markup, { key: "value" }, ["/static/vendor.bundle.js", "/static/index.bundle.js"], ["/static/index.css"] ) ); res.end(); } } catch (e) { console.error(e); res.status(500).send(e.message); }});

如果需要進行數據預抓取,可以考慮將數據掛載到頁面上進行傳遞。

代碼風格

詳細的 JavaScript 編程樣式指南已經遷移到了 Web 項目開發風格指南與 JavaScript 編程樣式指南,涵蓋了基本原則闡述、代碼風格、代碼格式化與語法檢測、項目架構等幾個部分。不過本部分建議是類似於 Create React APP 配置提交時自動進行格式化,首先需要安裝如下依賴:

npm install --save husky lint-staged prettier// oryarn add husky lint-staged prettier

然後在 package.json 中添加 Hook:

"scripts": { "precommit": "lint-staged", ...

同時添加 lint-staged 配置:

"dependencies": { // ... },+ "lint-staged": {+ "{src,stories}/**/*.{js,jsx,json,css}": [+ "prettier --single-quote --write",+ "git add"+ ]+ }, "scripts": {

這樣當我們提交代碼時就會自動使用 Prettier 優化代碼,不過需要注意的是這種配置僅作用於根目錄下;如果某個倉庫中包含了多個應用配置,那麼我們還需要在根目錄下單獨配置腳本。我們也可以使用 ./node_modules/.bin/prettier --single-quote --write "src/**/*.{js,jsx}" 來手動進行項目文件的格式化。


推薦閱讀:

WordPress 決定停止使用 React
用 ReactJs 創建Mac版的 keep
基於Decorator的組件擴展實踐
不一樣的 vue 實戰 (3): 布局與組件

TAG:webpack | React | Babel |