$ 如何安装 ? # https://git-scm.com/downloads $ 如何使用 ? # https://git-scm.com/doc || git --help 复制代码
⬆ back to topjavascript
.gitignore
有时,有些文件不但愿
Git
签入GitHub
。.gitignore
配置文件能够告诉Git
忽略哪些文件。css
$ touch .gitignore # starter/.gitignore # dependencies /node_modules # testing /coverage # production /build dist # misc .DS_Store # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .vscode 复制代码
⬆ back to tophtml
$ node 是什么 ? # http://nodejs.cn/ $ 如何安装 ? # http://nodejs.cn/download/ $ npm 是什么 ? # https://docs.npmjs.com/about-npm/ $ npm 如何使用 ? # 安装 Node.js 时附带安装了 npm || npm -v $ 建立包管理配置文件 package.json # https://docs.npmjs.com/creating-a-package-json-file $ package.json 文件中的要求 ? # https://docs.npmjs.com/files/package.json.html $ package-lock.json 是什么 ? # https://docs.npmjs.com/files/package-lock.json.html $ yarn 是什么? # https://yarn.bootcss.com/ $ yarn 如何安装 ? # https://yarn.bootcss.com/docs/install/#mac-stable $ yarn 如何使用 ? # https://yarn.bootcss.com/docs/ $ 初始化项目 # mkdir starter && npm init 复制代码
初始工程目录
与 package.json
的信息 ✅工程目录vue
└── starter
├── README.md
└── package.json
复制代码
package.jsonjava
{ "name": "starter", "version": "1.0.0", "description": "List of engineering builds for web applications", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/cllemon/starter.git" }, "keywords": [ "javascript", "typescript", "react" ], "author": "cllemon", "license": "MIT", "bugs": { "url": "https://github.com/cllemon/starter/issues" }, "homepage": "https://github.com/cllemon/starter#readme" } 复制代码
注意:如下描述中全部的包安装都采用
yarn
命令node
⬆ back to topreact
EditorConfig
能够帮助开发者在不一样的编辑器和IDE
之间定义和维护一致的代码风格。webpack
EditorConfig is awesome: editorconfig.orgios
$ touch .editoorconfig # starter/.editoorconfig root = true # 代表是最顶层的配置文件,发现设为 true 时,才会中止查找.editorconfig 文件。 [*] charset = utf-8 indent_style = space # tab 为 hard-tabs,space 为 soft-tabs。 indent_size = 2 # 规定每级缩进的列数和 soft-tabs 的宽度(空格数)。若是设定为 tab,则会使用 tab_width 的值。 end_of_line = lf # 定义换行符,支持 lf(UNIX/Linux采用换行符 LF 表示下一行)、cr(MAC OS系统)则采用回车符 CR 表示下一行) 和 crlf。 insert_final_newline = true # 设为 true 代表使文件以一个空白行结尾,false 反之 trim_trailing_whitespace = true # 设为 true 表示会除去换行行首的任意空白字符,false 反之。 [*.md] # 校验 markdown 文档 insert_final_newline = false trim_trailing_whitespace = false 复制代码
browserslist
是什么?
用于在不一样前端工具之间共享目标浏览器和
Node.js
版本的配置。例如Autoprefixer
,Stylelint
和babel-preset-env
。
browserslist
配置方式
当您将如下内容添加到
package.json
或 .browserslistrc配置文件中时,全部工具都会自动找到目标浏览器:
# package.json { "browserslist": { "production": [ // 生产环境配置 ">0.2%", // 支持市场份额大于 1% 的浏览器。 "not dead", // not(逻辑非)对 dead 取反,而浏览器被认为是 dead 条件是:最新的两个版本中发现其市场份额已经低于 0.5% 而且 24 个月内没有官方支持和更新。 "not op_mini all" // OperaMini or op_mini for Opera Mini. ], "development": [ // 开发环境配置 "last 1 chrome version", // 浏览器版本查询范围, chrome 最近的一个版本 "last 1 firefox version", "last 1 safari version" ] } } 或🔥 # .browserslistrc $ touch .browserslist [production] > .2% not dead not op_mini all [development] last 1 chrome version last 1 firefox version last 1 safari version 复制代码
本质上,
webpack
是一个现代JavaScript
应用程序的静态模块打包工具。当webpack
处理应用程序时,它会在内部构建一个 依赖图(dependency graph
),此依赖图会映射项目所需的每一个模块,并生成一个或多个bundle
。
安装与建立基本文件
$ mkdir src # 建立存放核心代码文件夹 $ cd src && touch index.js # 建立入口文件 $ yarn add -D webpack # 安装最新版本 webpack^4.41.2 $ yarn add -D webpack-cli # 安装 webpack v4+ 版本,所需的 webpack-cli^3.3.9 $ cd .. && touch webpack.config.js # 根目录,建立 webpack 基本配置文件 复制代码
工程目录
└── starter + ├── node_modules + ├── src + │ └── index.js + ├── webpack.config.js ├── package.json └── README.md 复制代码
A declarative, efficient, and flexible JavaScript library for building user interfaces.
安装与建立基本文件
$ yarn add react # 安装 react^16.10.2 $ yarn add react-dom # 安装 react-dom^16.10.2 $ mkdir public # 新建公共资源文件夹 $ cd public && touch index.html # 新建 html 文件 $ copy favicon.ico # 添加 网页图标 文件 $ cd .. # 回到根目录 复制代码
编写 index.html 文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="./favicon.ico" /> <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="This is a react application built from scratch with JavaScript, away from the cli tool." /> <title>Starter</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html> 复制代码
编写 index.js 文件
import React from 'react'; import ReactDom from 'react-dom'; const App = () => <h1>Hello, world!</h1> ReactDom.render(<App />, document.getElementById('root')); 复制代码
注意:因为浏览器不支持最新的 JavaScript 语法和 react jsx 的语法解析,因此咱们须要一个编译器帮助咱们。
Babel 是一个工具链,主要用于在旧的浏览器或环境中将 ECMAScript 2015+ 代码转换为向后兼容版本的 JavaScript 代码。
babel 安装
$ yarn add -D @babel/core # Babel 编译器核心模块 $ yarn add -D @babel/preset-env # 是一个智能预设,它使您可使用最新的JavaScript,而无需微观管理目标环境所需的语法转换 $ yarn add -D @babel/preset-react # react 智能预设, 包含了解析 jsx 等插件 $ yarn add -D babel-loader # Babel loader for webpack 该软件包容许使用 Babel 和 webpack 来转译 JavaScript 文件。 $ touch .babelrc # 新建 babel 配置文件 复制代码
babel 配置
// .babelrc { "presets": ["@babel/preset-env", "@babel/preset-react"], } 复制代码
编写 webpack 配置
// starter/webpack.config.js const path = require('path'); module.exports = function() { const baseConfig = { entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel-loader' }, ] } }; return baseConfig; }; 复制代码
修改 package.json 添加 webpack 命令, 快捷运行
{ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", + "build": "webpack --color --progress" } } 复制代码
修改 index.html 引入打包以后的 bundle.js 文件
... <div id="root"></div> + <script src="../dist/bundle.js"></script> ... 复制代码
运行项目
$ yarn build # 打包文件 Hash: 6e4adf36d533e9d646c0 Version: webpack 4.41.2 Time: 693ms Built at: 2019-10-19 11:41:22 Asset Size Chunks Chunk Names bundle.js 1.09 MiB main [emitted] main Entrypoint main = bundle.js [./src/index.js] 233 bytes {main} [built] # 浏览器 打开 index.html 查看效果 复制代码
工程目录
└── starter + ├── dist + │ └── bundle.js ├── node_modules + ├── public + │ ├── favicon.ico + │ └── index.html ├── src │ └── index.js ├── webpack.config.js ├── package.json ├── README.md + └── yarn.lock 复制代码
webpack-dev-server 为你提供了一个简单的 web server,而且具备 live reloading(实时从新加载) 功能。
安装
$ yarn add -D webpack-dev-server # 用于快速开发应用程序 复制代码
添加相应配置
// starter/webpack.config.js const path = require('path'); module.exports = function() { const baseConfig = { devtool: 'inline-source-map', // 控制是否生成,以及如何生成 source map entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel-loader' }, ] }, + devServer: { + contentBase: path.resolve(__dirname, 'public'), // 告诉服务器从哪一个目录中提供内容 + historyApiFallback: true, // 启用当使用 HTML5 History API 时,任意的 404 响应均可能须要被替代为 index.html。 + compress: true, // 一切服务都启用 gzip 压缩 + open: true, // 告诉 dev-server 在 server 启动后打开浏览器 + port: 3000, // 指定要监听请求的端口号 + stats: 'errors-only', // 精确控制要显示的 bundle 信息 (在 bundle 中只显示错误) + } }; return baseConfig; }; 复制代码
修改 package.json 添加 webpack 命令, 快捷运行
{ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack --color --progress", + "server": "webpack-dev-server --color --progress" } } 复制代码
--color
: 启用/禁用控制台的彩色输出;--progress
: 将运行进度输出到控制台。
修改 index.html 主文件 bundle.js 路径
... <div id="root"></div> - <script src="../dist/bundle.js"></script> + <script src="bundle.js"></script> ... 复制代码
运行项目
$ yarn server # 结果: $ webpack-dev-server --color --progress 10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public ℹ 「wds」: 404s will fallback to /index.html ℹ 「wdm」: Compiled successfully. 复制代码
打开
http://localhost:3000/
将显示Hello, world!
; 修改src/index.js
将会刷新浏览器实时更新修改。Try it!
存在问题或待改进提高点
webpack.config.js
有些配置咱们只但愿在开发环境有,而在生产环境应有其特定配置)带着这些问题,继续吧!👍
安装
$ yarn add -D cross-env # Cross platform setting of environment scripts 复制代码
修改 package.json webpack 命令
{ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "webpack --color --progress", + "build": "cross-env NODE_ENV=production webpack --color --progress", - "server": "webpack-dev-server --color --progress" + "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress" } } 复制代码
为 webpack.config.js 添加相应配置
// starter/webpack.config.js const path = require('path'); + const IS_PROD = process.env.NODE_ENV === 'production'; module.exports = function() { const baseConfig = { + mode: IS_PROD ? 'production' : 'development', - devtool: 'inline-source-map', // 控制是否生成,以及如何生成 source map + devtool: IS_PROD ? false : 'inline-source-map', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel-loader' }, ] }, - devServer: { - contentBase: path.resolve(__dirname, 'public'), // 告诉服务器从哪一个目录中提供内容 - historyApiFallback: true, // 启用当使用 HTML5 History API 时,任意的 404 响应均可能须要被替代为 index.html。 - compress: true, // 一切服务都启用 gzip 压缩 - open: true, // 告诉 dev-server 在 server 启动后打开浏览器 - port: 3000, // 指定要监听请求的端口号 - stats: 'errors-only', // 精确控制要显示的 bundle 信息 (在 bundle 中只显示错误) - } }; + if (!IS_PROD) { + baseConfig.devServer = { + contentBase: path.resolve(__dirname, 'public'), // 告诉服务器从哪一个目录中提供内容 + historyApiFallback: true, // 启用当使用 HTML5 History API 时,任意的 404 响应均可能须要被替代为 index.html。 + compress: true, // 一切服务都启用 gzip 压缩 + open: true, // 告诉 dev-server 在 server 启动后打开浏览器 + port: 3000, // 指定要监听请求的端口号 + stats: 'errors-only', // 精确控制要显示的 bundle 信息 (在 bundle 中只显示错误) + }; + } return baseConfig; }; 复制代码
模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它容许在运行时更新全部类型的模块,而无需彻底刷新。
为 webpack.config.js 添加相应配置
// starter/webpack.config.js const path = require('path'); + const webpack = require('webpack'); const IS_PROD = process.env.NODE_ENV === 'production'; module.exports = function() { const baseConfig = { mode: IS_PROD ? 'production' : 'development', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel-loader' }, ] }, + plugins: [] }; if (!IS_PROD) { baseConfig.devServer = { contentBase: path.resolve(__dirname, 'public'), // 告诉服务器从哪一个目录中提供内容 historyApiFallback: true, // 启用当使用 HTML5 History API 时,任意的 404 响应均可能须要被替代为 index.html。 compress: true, // 一切服务都启用 gzip 压缩 open: true, // 告诉 dev-server 在 server 启动后打开浏览器 port: 3000, // 指定要监听请求的端口号 stats: 'errors-only', // 精确控制要显示的 bundle 信息 (在 bundle 中只显示错误) + hot: true // 启用 webpack 的 模块热替换 功能 }; + baseConfig.plugins.concat([ + new webpack.HotModuleReplacementPlugin() // 热替换模块插件 + ]); } return baseConfig; }; 复制代码
修改 src/index.js 文件
- import React from 'react'; + import React, { useState } from 'react'; import ReactDom from 'react-dom'; - const App = () => <h1>Hello, world!</h1>; + const App = () => { + const [title, setTitle] = useState('hello, world!'); + const reversedTitle = () => + setTitle( + title + .split('') + .reverse() + .join('') + ); + return ( + <div> + <h1>{ title }</h1> + <button type='button' onClick={reversedTitle}> + reversed title + </button> + </div> + ); + }; + if (module.hot) { + module.hot.accept(); + } ReactDom.render(<App />, document.getElementById('root')); 复制代码
运行项目
$ yarn server # 结果: $ cross-env NODE_ENV=development webpack-dev-server --color --progress 10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public ℹ 「wds」: 404s will fallback to /index.html ℹ 「wdm」: Compiled successfully. # 浏览器 console [HMR] Waiting for update signal from WDS... log.js:24 [WDS] Hot Module Replacement enabled. client:48 [WDS] Live Reloading enabled. client:52 复制代码
打开
http://localhost:3000/
修改src/index.js
实现了未刷新浏览器更新修改。Try it!
存在问题或待改进提高点
带着这个问题,继续吧!✈️
实时调整React组件。
说明
安装
$ yarn add react-hot-loader $ yarn add @hot-loader/react-dom # 替换了相同版本的 react-dom 软件包,但附加了一些补丁以支持热重装。 复制代码
将 "react-hot-loader/babel"
添加到您的 .babelrc
中
{ "presets": ["@babel/preset-env", "@babel/preset-react"], + "plugins": ["react-hot-loader/babel"] } 复制代码
重置 react-dom
兼容 hooks
... moduele.exports = function () { ... + resolve: { + alias: { + 'react-dom': '@hot-loader/react-dom' // react-hot-loader 兼容 hook 写法 + } + }, ... } ... 复制代码
修改 src/index.js 主文件,将根组件标记为 hot-exported
+ import { hot } from 'react-hot-loader'; import React, { useState } from 'react'; import ReactDom from 'react-dom'; - const App = () => { + const App = hot(module)(() => { const [title, setTitle] = useState('hello, world!'); const reversedTitle = () => setTitle( title .split('') .reverse() .join('') ); return ( <div> <h1>{title}</h1> <button type='button' onClick={reversedTitle}> reversed title! </button> </div> ); - }; + }); - if (module.hot) { - module.hot.accept(); - } ReactDom.render(<App />, document.getElementById('root')); 复制代码
运行项目
$ yarn server # 结果: $ cross-env NODE_ENV=development webpack-dev-server --color --progress 10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public ℹ 「wds」: 404s will fallback to /index.html ℹ 「wdm」: Compiled successfully. ℹ 「wdm」: Compiling... ℹ 「wdm」: Compiled successfully. # 浏览器 console [HMR] Waiting for update signal from WDS... log.js:24 [WDS] App hot update... reloadApp.js:19 [HMR] Checking for updates on the server... log.js:24 [HMR] Updated modules: log.js:24 [HMR] - ./src/index.js log.js:24 [HMR] App is up to date. log.js:24 复制代码
打开
http://localhost:3000/
, 点击reversed title
而后修改src/index.js
实现了未刷新浏览器保留组件状态的更新修改。Try it!
阶段结语
样式是前端组件重要组成部分,而 Sass 让 CSS 语言更强大、优雅;有助于保持大型样式表结构良好。
注意:本项目引入 sass ,固然你也能够不引入或者引入其它,如:less、stylus。
安装
$ yarn add -D node-sass # Node-sass是一个库,提供了 Node.js 与 LibSass(流行的样式表预处理器Sass的C版本)的绑定。 它使您可以以惊人的速度经过链接中间件自动将 .scss 文件本地编译为 css $ yarn add -D sass-loader # Compiles Sass to CSS $ yarn add -D css-loader # The css-loader interprets @import and url() like import/require() and will resolve them. $ yarn add -D style-loader # Inject CSS into the DOM. 复制代码
注:sass基于Ruby语言开发而成,所以安装sass前须要安装Ruby。(注:mac下自带Ruby无需在安装Ruby!)
为何须要 node-sass : 由于 sass-loader 的 peerDependencies 声明了其依赖 node-sass,因此须要预装,不然警告。
配置:修改 webpack.config.js 增长css/sass解析能力
... moduele.exports = function () { ... module: { rules: [ ... + { + test: /\.(sa|sc|c)ss$/, + exclude: /node_modules/, + use: [ + { + loader: 'style-loader' + }, + { + loader: 'css-loader', + options: { + sourceMap: !IS_PROD + } + }, + { + loader: 'sass-loader', + options: { + sourceMap: !IS_PROD + } + } + ] + } ] } ... } ... 复制代码
新增 src/index.scss 和 style/global.css 样式文件
$ cd src && touch index.scss $ mkdir style && cd style $ touch global.css && touch reset.css 复制代码
// src/style/reset.css
# reset 重置浏览器初始样式,具体样式参见项目 src/style/reset.css
// src/style/global.css
@import url('./reset.css');
// src/index.scss
.app {
background-color: red;
}
复制代码
修改 src/index.js 导入样式表
import { hot } from 'react-hot-loader'; import React, { useState } from 'react'; import ReactDom from 'react-dom'; + import './style/global.css'; + import './index.scss'; const App = hot(module)(() => { const [title, setTitle] = useState('hello, world!'); const reversedTitle = () => setTitle( title .split('') .reverse() .join('') ); return ( - <div> + <div className='app'> <h1>{title}</h1> <button type='button' onClick={reversedTitle}> reversed title! </button> </div> ); }); ReactDom.render(<App />, document.getElementById('root')); 复制代码
运行项目
$ yarn server # 结果: $ cross-env NODE_ENV=development webpack-dev-server --color --progress 10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public ℹ 「wds」: 404s will fallback to /index.html ℹ 「wdm」: Compiled successfully. 复制代码
打开
http://localhost:3000/
, 如你所写,出现一个红色背景。Try it!
问题与改进点🤔
CSS
文件而且添加浏览器前缀到 CSS
内容里;postcss/autoprefixer
css-modules
去解决这个问题。固然你也能够采用严格命名规范绕开这个问题,如:BEM。那继续吧!💪
安装
$ yarn add - D postcss-loader # 用于webpack的Loader以使用PostCSS处理CSS $ yarn add -D autoprefixer # Parse CSS and add vendor prefixes to rules by Can I Use 复制代码
新建 postcss 配置文件
$ touch postcss.config.js # 新建 postcss 配置文件 # starter/postcss.config.js 添加 autoprefixer 插件 module.exports = { plugins: { autoprefixer: {}, } }; 复制代码
添加 webpack postcss 配置
... moduele.exports = function () { ... module: { rules: [ ... { - test: /\.(sa|sc|c)ss$/, + test: /\.(sa|sc)ss$/, exclude: /node_modules/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', + options: { + sourceMap: !IS_PROD, + importLoaders: 2, // 启用/禁用或设置在CSS加载程序以前应用的加载程序的数量 + modules: { + context: path.resolve(__dirname, 'src'), // 容许为本地标识符名称从新定义基本的加载程序上下文。 + localIdentName: '[name]__[local]-[hash:base64:5]' // 使用 localIdentName 查询参数配置生成类名 + } + } }, + { + loader: 'postcss-loader' + } { loader: 'sass-loader', options: { sourceMap: !IS_PROD } } ] }, + { + test: /\.css$/, + exclude: /node_modules/, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + sourceMap: !IS_PROD + } + }, + 'post-loader' + ] + } ] } ... } ... 复制代码
postcss
: 一个用JavaScript
转换CSS
的工具
css-loader
提供CSS
模块及其配置
修改 src/index.js 类名写法
import { hot } from 'react-hot-loader'; import React, { useState } from 'react'; import ReactDom from 'react-dom'; import './style/global.css'; - import './index.scss'; + import styles from './index.scss'; const App = hot(module)(() => { const [title, setTitle] = useState('hello, world!'); const reversedTitle = () => setTitle( title .split('') .reverse() .join('') ); return ( - <div className='app'> + <div className={styles.app}> <h1>{title}</h1> <button type='button' onClick={reversedTitle}> reversed title! </button> </div> ); }); ReactDom.render(<App />, document.getElementById('root')); 复制代码
运行项目
$ yarn server # 结果 $ cross-env NODE_ENV=development webpack-dev-server --color --progress 10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from /Users/gt/LEMON/starter/public ℹ 「wds」: 404s will fallback to /index.html ℹ 「wdm」: Compiled successfully. 复制代码
打开
http://localhost:3000/
查看,是否如你所写!
yarn build
$ yarn build # 结果 $ cross-env NODE_ENV=production webpack --color --progress Hash: 4f40eeb2a231c73dacd9 Version: webpack 4.41.2 Time: 4142ms Built at: 2019-10-21 10:54:37 Asset Size Chunks Chunk Names bundle.js 136 KiB 0 [emitted] main Entrypoint main = bundle.js [5] ./src/index.scss 498 bytes {0} [built] [7] ./src/index.js 1.57 KiB {0} [built] [8] (webpack)/buildin/harmony-module.js 573 bytes {0} [built] [13] ./src/style/global.css 457 bytes {0} [built] [14] ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/src!./src/style/global.css 237 bytes {0} [built] [15] ./node_modules/css-loader/dist/cjs.js!./src/style/reset.css 1.28 KiB {0} [built] [16] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/dist/cjs.js!./src/index.scss 238 bytes {0} [built] + 11 hidden modules ✨ Done in 5.90s. 复制代码
咱们看到这只打出一个
bundle.js
这显然作的还不够。接下来,咱们作几点改变!
到目前为止,咱们都是在 index.html 文件中手动引入全部资源,然而随着应用程序增加,而且一旦开始 在文件名中使用 hash] 并输出 多个 bundle,若是继续手动管理 index.html 文件,就会变得困难起来。
修改 webpack - output
const path = require('path'); const webpack = require('webpack'); const IS_PROD = process.env.NODE_ENV === 'production'; ... output: { path: path.resolve(__dirname, 'dist'), - publicPath: '/', + publicPath: IS_PROD ? '/starter/' : '/', // 公共路径 - filename: 'bundle.js' + filename: IS_PROD ? '[name].[contenthash:8].js' : '[name].js', // 输出文件的文件名 + chunkFilename: IS_PROD ? 'chunks/[name].[contenthash:8].js' : '[name].js', // 非入口(non-entry) chunk 文件的名称 }, ... 复制代码
HtmlWebpackPlugin
$ yarn add -D html-webpack-plugin # 安装插件 复制代码
<!-- starter/webpack.config.js --> const path = require('path'); const webpack = require('webpack'); const IS_PROD = process.env.NODE_ENV === 'production'; + const HtmlWebpackPlugin = require('html-webpack-plugin') ... - plugins: [] + plugins: [ + new HtmlWebpackPlugin({ + title: 'Starter', + filename: 'index.html', + template: path.resolve(__dirname, 'public/index.html'), + minify: IS_PROD + ? { + removeComments: true, + collapseWhitespace: true, + removeAttributeQuotes: true, + collapseBooleanAttributes: true, + removeScriptTypeAttributes: true + } + : {} + }), + ] ... 复制代码
修改 public/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="./favicon.ico" /> <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="This is a react application built from scratch with JavaScript, away from the cli tool." /> - <title>Starter</title> + <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> - <script src="bundle.js"></script> </body> </html> 复制代码
经过上述配置,让咱们来看看效果吧
$ yarn build # 结果 $ cross-env NODE_ENV=production webpack --color --progress Hash: 6bb93a13b6a8a7926f58 Version: webpack 4.41.2 Time: 4418ms Built at: 2019-10-21 11:47:05 Asset Size Chunks Chunk Names index.html 553 bytes [emitted] main.2f781ad1.js 136 KiB 0 [emitted] [immutable] main Entrypoint main = main.2f781ad1.js [5] ./src/index.scss 498 bytes {0} [built] [7] ./src/index.js 1.57 KiB {0} [built] [8] (webpack)/buildin/harmony-module.js 573 bytes {0} [built] [13] ./src/style/global.css 457 bytes {0} [built] [14] ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/src!./src/style/global.css 237 bytes {0} [built] [15] ./node_modules/css-loader/dist/cjs.js!./src/style/reset.css 1.28 KiB {0} [built] [16] ./node_modules/css-loader/dist/cjs.js??ref--5-1!./node_modules/postcss-loader/src!./node_modules/sass-loader/dist/cjs.js!./src/index.scss 238 bytes {0} [built] + 11 hidden modules Child html-webpack-plugin for "index.html": 1 asset Entrypoint undefined = index.html [0] ./node_modules/html-webpack-plugin/lib/loader.js!./public/index.html 858 bytes {0} [built] [2] (webpack)/buildin/global.js 472 bytes {0} [built] [3] (webpack)/buildin/module.js 497 bytes {0} [built] + 1 hidden module ✨ Done in 6.04s. 复制代码
<!-- starter/dist/index.html --> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><link rel=icon href=./favicon.ico><meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"><meta name=theme-color content=#000000><meta name=description content="This is a react application built from scratch with JavaScript, away from the cli tool."><title>React App TS</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id=root></div><script src=/starter/main.2f781ad1.js></script></body></html> 复制代码
注:若是你仔细看了咱们的输出,你会发现
main.2f781ad1.js size=136KiB
, 而咱们的代码却量却不多,若是你打开该文件你会发现它包含了react.production.min.js
babel
所需的帮助函数等。
mini-css-extract-plugin - 分离 css 代码
webpack 默认把 css 和 js 打到一个文件,该插件将CSS提取到单独的文件中。它为每一个包含CSS的JS文件建立一个CSS文件。
$ yarn add -D mini-css-extract-plugin # 安装 复制代码
<!-- starter/webpack.config.js --> ... + const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel-loader' }, { test: /\.(sa|sc)ss$/, exclude: /node_modules/, use: [ { - loader: 'style-loader' + loader: IS_PROD ? MiniCssExtractPlugin.loader : 'style-loader', + options: IS_PROD ? { publicPath: '../' } : {} }, { loader: 'css-loader', options: { sourceMap: false, importLoaders: 2, modules: { context: path.resolve(__dirname, 'src'), localIdentName: '[name]__[local]-[hash:base64:5]' } } }, { loader: 'postcss-loader' }, { loader: 'sass-loader' } ] }, { test: /\.css$/, exclude: /node_modules/, - use: ['style-loader', 'css-loader', 'postcss-loader'] + use: [ + { + loader: IS_PROD ? MiniCssExtractPlugin.loader : 'style-loader', + options: IS_PROD ? { publicPath: '../' } : {} + }, + 'css-loader', + 'postcss-loader' + ] } ] }, + plugins: [ ..., + new MiniCssExtractPlugin({ + filename: IS_PROD ? 'css/[name].[contenthash:8].css' : 'css/[name].css', + chunkFilename: IS_PROD ? 'css/[name].[contenthash:8].css' : 'css/[name].css' + }) ] 复制代码
$ yarn build $ cross-env NODE_ENV=production webpack --color --progress Hash: 95fccf0e0844c2df588f Version: webpack 4.41.2 Time: 4416ms Built at: 2019-10-21 13:56:23 Asset Size Chunks Chunk Names ! css/main.f9cee851.css 1.08 KiB 0 [emitted] [immutable] main index.html 605 bytes [emitted] ! main.ced0f821.js 131 KiB 0 [emitted] [immutable] main Entrypoint main = css/main.f9cee851.css main.ced0f821.js 复制代码
屡次打包以后咱们发现多处不少上次结果文件,这显然不能忍受 w(゚Д゚)w; 咱们但愿在每次构建以前删除以前构建生成的文件夹。
clean-webpack-plugin 保持目录清洁
用于在构建以前删除您的构建文件夹
$ yarn add -D clean-webpack-plugin # 安装 复制代码
<!-- starter/webpack.config.js --> ... + const { CleanWebpackPlugin } = require('clean-webpack-plugin'); ... plugins: [ ..., + new CleanWebpackPlugin() ] ... 复制代码
试试看👀,清理干净了 (。・∀・)ノ゙ Try it!
optimization.splitChunks 将公共的依赖模块提取到已有的 entry chunk 中
<!-- starter/webpack.config.js --> ... module.exports = function () { const baseConfig = { ... } + if (IS_PROD) { + baseConfig.optimization = { + minimizer: [ + // Automatically split vendor and commons + splitChunks: { + chunks: 'all' + } + ] + } + } return baseConfig; } 复制代码
$ yarn build # 打包查看效果 # 结果 $ cross-env NODE_ENV=production webpack --color --progress Hash: ebe27d1c4dc54ff22c4b Version: webpack 4.41.2 Time: 4470ms Built at: 2019-10-21 14:28:59 Asset Size Chunks Chunk Names ! chunks/vendors~main.f501917c.js 129 KiB 1 [emitted] [immutable] vendors~main css/main.f9cee851.css 1.08 KiB 0 [emitted] [immutable] main ! index.html 667 bytes [emitted] ! main.76c9ecec.js 2.54 KiB 0 [emitted] [immutable] main Entrypoint main = chunks/vendors~main.f501917c.js css/main.f9cee851.css main.76c9ecec.js # 注意:若是你仔细看 chunks/vendors~main.f501917c.js 你会发现 与 react 相关的库 #(react.production.min.js、react-dom.production.min.js、scheduler.production.min.js)和你代 # 码所引用的公共库都将被提取出来,防止重复引用。 复制代码
webpack 4: Code Splitting, chunk graph and the splitChunks optimization
@babel/plugin-transform-runtime 一个插件,可从新使用Babel注入的帮助程序代码以节省代码大小。
$ yarn add -D @babel/plugin-transform-runtime
复制代码
<!-- starter/postcss.config.js --> { "presets": [ "@babel/preset-env", "@babel/preset-react" ], "plugins": [ + "@babel/plugin-transform-runtime", "react-hot-loader/babel" ] } 复制代码
$ yarn build # 结果 $ cross-env NODE_ENV=production webpack --color --progress Hash: 6425898f896ed7244e2b Version: webpack 4.41.2 Time: 4510ms Built at: 2019-10-21 15:18:47 Asset Size Chunks Chunk Names ! chunks/vendors~main.e9e35553.js 130 KiB 1 [emitted] [immutable] vendors~main css/main.f9cee851.css 1.08 KiB 0 [emitted] [immutable] main index.html 667 bytes [emitted] ! main.5fb316df.js 2.07 KiB 0 [emitted] [immutable] main Entrypoint main = chunks/vendors~main.e9e35553.js css/main.f9cee851.css main.5fb316df.js # 能够比对上次构建结果,主文件减小了一些。 复制代码
webpack.DefinePlugin 容许建立一个在编译时能够配置的全局常量
插件可配置一些全局变量,在构建时将会对代码内引用的这些变量进行替换。好比:NODE_ENV(经常使用于处理生产环境与开发环境)。若是在开发构建中,而不在发布构建中执行日志记录,则可使用全局常量来决定是否记录日志。这就是 DefinePlugin 的用处,设置它,就能够忘记开发环境和生产环境构建的规则。
<!-- starter/webpack.config.js --> ... plugins: [ ..., + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify(process.env.NODE_ENV), + } + }) ] ... 复制代码
这里若是你的代码没有对区分环境,作特定处理(去除开发环境下的代码)则,包尺寸不变。
$ yarn add -D uglifyjs-webpack-plugin
复制代码
<!-- starter/webpack.config.js --> ... + const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin'); ... if (IS_PROD) { baseConfig.optimization = { + minimizer: [ + new UglifyjsWebpackPlugin({ + exclude: /node_modules/, + sourceMap: false, // 使用源映射将错误消息位置映射到模块(这会减慢编译速度)。若是您使用本身的缩小功能,请阅读缩小部分以正确处理源地图。 + cache: true, // 启用文件缓存 + parallel: true // 使用多进程并行运行可提升构建速度。并发运行的默认数量:os.cpus().length - 1. + }) + ], splitChunks: { chunks: 'all', } }; } 复制代码
$ yarn build # 打包验证 ✅ # 结果 $ cross-env NODE_ENV=production webpack --color --progress Hash: 3f450244bccc719560c5 Version: webpack 4.41.2 Time: 2209ms Built at: 2019-10-21 16:25:18 Asset Size Chunks Chunk Names ! chunks/vendors~main.1a122e64.js 129 KiB 1 [emitted] [immutable] vendors~main css/main.f9cee851.css 1.08 KiB 0 [emitted] [immutable] main index.html 667 bytes [emitted] main.e82008bc.js 2.07 KiB 0 [emitted] [immutable] main Entrypoint main = chunks/vendors~main.1a122e64.js css/main.f9cee851.css main.e82008bc.js 复制代码
注意:
uglifyjs-webpack-plugin v2.x
版本基于uglify-js
,没法支持ES6
的压缩
咱们用
terser-webpack-plugin
替换uglifyjs-webpack-plugin
$ yarn add -D terser-webpack-plugin
复制代码
<!-- starter/webpack.config.js --> - const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin'); + const TerserPlugin = require('terser-webpack-plugin'); if (IS_PROD) { baseConfig.optimization = { minimizer: [ - new UglifyjsWebpackPlugin({ - exclude: /node_modules/, - sourceMap: false, - cache: true, - parallel: true - }), + new TerserPlugin({ + // Terser minify options. + terserOptions: { + parse: { + // We want terser to parse ecma 8 code. However, we don't want it + // to apply any minification steps that turns valid ecma 5 code + // into invalid ecma 5 code. This is why the 'compress' and 'output' + // sections only apply transformations that are ecma 5 safe + ecma: 8, + }, + compress: { + ecma: 5, + // display warnings when dropping unreachable code or unused declarations etc. + warnings: false, + // apply certain optimizations to binary nodes + // Disabled because of an issue with Uglify breaking seemingly valid code: + // Pending further investigation: https://github.com/mishoo/UglifyJS2/issues/2011 + comparisons: false, + // inline calls to function with simple/return statement: + // Disabled because of an issue with Terser breaking valid code: + // Pending further investigation: https://github.com/terser-js/terser/issues/120 + inline: 2, // inline functions with arguments + }, + mangle: { + // Pass true to work around the Safari 10 loop iterator bug "Cannot declare a let variable twice". + // See also: the safari10 output option. + safari10: true, + }, + // Added for profiling in devtools + keep_classnames: true, + keep_fnames: true, + output: { + ecma: 5, + // pass true or "all" to preserve all comments, "some" to preserve some comments, + // a regular expression string (e.g. /^!/) or a function. + comments: false, + // escape Unicode characters in strings and regexps (affects directives with non-ascii characters becoming invalid) + // Turned on because emoji and regex is not minified properly using default + ascii_only: true, + }, + }, + // Use multi-process parallel running to improve the build speed. + //Default number of concurrent runs: os.cpus().length - 1. + parallel: true, + cache: true, // Enable file caching + }), ], splitChunks: { chunks: 'all', } }; } 复制代码
$ yarn build $ cross-env NODE_ENV=production webpack --color --progress Hash: dbf5243d5591e4ac0268 Version: webpack 4.41.2 Time: 2461ms Built at: 2019-10-21 17:52:26 Asset Size Chunks Chunk Names ! chunks/vendors~main.ae62441b.js 130 KiB 1 [emitted] [immutable] vendors~main ! chunks/vendors~main.ae62441b.js.LICENSE 790 bytes [emitted] css/main.f9cee851.css 1.08 KiB 0 [emitted] [immutable] main index.html 667 bytes [emitted] ! main.2130b172.js 2.52 KiB 0 [emitted] [immutable] main Entrypoint main = chunks/vendors~main.ae62441b.js css/main.f9cee851.css main.2130b172.js 复制代码
optimize-css-assets-webpack-plugin - 优化/减小CSS资产
$ yarn add -D optimize-css-assets-webpack-plugin # 压缩 CSS $ yarn add -D postcss-safe-parser # 查找并修复 CSS 语法错误 复制代码
<!-- starter/webpack.config.js --> ... + const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); + const SafePostCssParser = require('postcss-safe-parser'); if (IS_PROD) { baseConfig.optimization = { minimizer: [ ... + new OptimizeCSSAssetsPlugin({ + // The options passed to the cssProcessor, defaults to {} + // cssProcessor: The CSS processor used to optimize \ minimize the CSS, defaults to cssnano. + // This should be a function that follows cssnano.process interface + // (receives a CSS and options parameters and returns a Promise). + cssProcessorOptions: { + parser: SafePostCssParser, + map: false, + }, + }) ], ... }; } ... 复制代码
$ yarn build # 打包实验 ✅ # 结果 $ cross-env NODE_ENV=production webpack --color --progress Hash: dbf5243d5591e4ac0268 Version: webpack 4.41.2 Time: 3543ms Built at: 2019-10-21 20:17:23 Asset Size Chunks Chunk Names chunks/vendors~main.ae62441b.js 130 KiB 1 [emitted] [immutable] vendors~main chunks/vendors~main.ae62441b.js.LICENSE 790 bytes [emitted] ! css/main.f9cee851.css 869 bytes 0 [emitted] [immutable] main index.html 667 bytes [emitted] main.2130b172.js 2.52 KiB 0 [emitted] [immutable] main Entrypoint main = chunks/vendors~main.ae62441b.js css/main.f9cee851.css main.2130b172.js 复制代码
从输出的 bundle 中排除依赖; 防止将某些 import 的包打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
CDN - 此步可忽略
<!-- starter/webpack.config.js --> ... module.exports = function() { const baseConfig = { ... resolve: { alias: { 'react-dom': '@hot-loader/react-dom' // react-hot-loader 兼容 hook 写法 } }, + externals: { + react: 'React', + 'react-dom': 'ReactDOM' + }, ... } ... <!-- starter/public/index.html --> ... <div id="root"></div> + <script crossorigin src="https://unpkg.com/react@16.10.2/umd/react.production.min.js"></script> + <script crossorigin src="https://unpkg.com/@hot-loader/react-dom@16.10.2/umd/react-dom.production.min.js"></script> ... 复制代码
$ yarn build
$ cross-env NODE_ENV=production webpack --color --progress
Hash: 6bb2de2632bdaf2dc081
Version: webpack 4.41.2
Time: 2557ms
Built at: 2019-10-21 21:35:38
Asset Size Chunks Chunk Names
css/main.34dd0d40.css 869 bytes 0 [emitted] [immutable] main
index.html 811 bytes [emitted]
main.da7fbe78.js 3.85 KiB 0 [emitted] [immutable] main
Entrypoint main = css/main.34dd0d40.css main.da7fbe78.js
复制代码
1. CDN是什么?使用CDN有什么优点?
2. 几个 CDN 公共库:cdnjs、jsdelivr、unpkg
3. 为提升访问速度,最好把前端不常更新的类库,如,react、react-dom、axios、moment等从输出的 bundle 中排除依赖
4. 提示,最好本身弄个,用本身的老是来得保险一些 🤡
└── starter + ├── dist + │ └── chunks + │ │ ├── vendors~main.ae62441b.js + │ │ └── vendors~main.ae62441b.js.LICENSE + ├── css + │ │ └── main.f9cee851.css + │ ├── index.html + │ └── main.2130b172.js ├── node_modules ├── public │ ├── favicon.ico │ └── index.html ├── src + │ └── style + │ | ├── global.css + │ | └── reset.css | ├── index.js + │ ├── index.scss + ├── postcss.config.js ├── webpack.config.js ├── package.json ├── README.md ├── LICENSE └── yarn.lock 复制代码
前端单页应用,路由必不可少,目前主流框架都有配套路由插件,这里配合所选框架引入 react-router-dom
安装
$ yarn add react-router-dom
复制代码
新建路由配置文件夹
$ cd src && mkdir router # 新建 router 文件夹 $ cd router $ touch index.js # 新建路由配置文件 $ touch list.js # 新建路由表文件 复制代码
编写路由配置及路由表
路由配置 - src/router/index.js
import React from 'react'; import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'; import routes from './list'; function RouterView(route) { return ( <Route path={route.path} render={(props) => { if (route.redirect) { return <Redirect to={route.redirect} />; } return ( <route.component {...props} render={() => ( <Switch> {route.routes.map((children) => ( <RouterView key={children.path} {...children} /> ))} </Switch> )} /> ); }} /> ); } export default function Router() { return ( <BrowserRouter> <Switch> {routes.map((route) => ( <RouterView key={route.path} {...route} /> ))} </Switch> </BrowserRouter> ); } 复制代码
这里根据路由配置文档编写的,仅作 DEMO 使用;详情参阅 react-router: Route Config
路由表 - src/router/list.js
import Github from '../views/Github/Github'; import Setting from '../views/Setting/Setting'; const routes = [ { path: '/', exact: true, redirect: '/github' }, { path: '/github', component: Github, }, { path: '/setting', component: Setting, } ]; export default routes; 复制代码
这里的命名你能够随意建立🙄
新建 Setting、GitHub 页面,并编写
# 新建 Setting、GitHub 页面 $ cd src/views $ mkdir Github && cd Github $ touch Github.js && touch Github.scss $ cd .. $ mkdir Setting && cd Setting $ touch Setting.js && touch Setting.scss $ cd .. 复制代码
// starter/Github/Github.js import React from 'react'; import { useHistory } from 'react-router-dom'; import styles from './Github.scss'; function Github() { const history = useHistory(); function handleClick() { history.push('/setting'); } return ( <div className={`${styles.root}`}> <h1>Github</h1> <div className={`${styles.bg} ${styles.wh}`}> {`当前环境: ${process.env.NODE_ENV}`} </div> <button type='button' onClick={handleClick}> Go setting </button> </div> ); } export default Github; // starter/Setting/Setting.js import React from 'react'; import { useHistory } from 'react-router-dom'; import styles from './Setting.scss'; function Setting() { const history = useHistory(); function handleClick() { history.push('/github'); } return ( <div className={`${styles.root}`}> <h1>Setting</h1> <div className={`${styles.bg} ${styles.wh}`}> {`当前环境: ${process.env.NODE_ENV}`} </div> <button type='button' onClick={handleClick}> Go github </button> </div> ); } export default Setting; 复制代码
// starter/Setting/Setting.scss .root { .wh { width: 200px; height: 180px; } .bg { text-align: center; line-height: 180px; background: no-repeat url('~assets/images/logo.png'); } } // starter/Github/Github.scss .root { .wh { width: 200px; height: 200px; } .bg { text-align: center; line-height: 200px; background: no-repeat url('~assets/images/logo.png'); } } 复制代码
因为样式引入图片,因此咱们新建资源存放文件夹,用来存放这些资源
$ cd src && mkdir assets $ cd assets && mkdir images $ cd images $ copy logo.png # 这里的图标是官网搂过来的,🤣 复制代码
修改咱们的主文件 src/index.js
import { hot } from 'react-hot-loader'; - import React, { useState } from 'react'; + import React from 'react'; import ReactDom from 'react-dom'; import './style/global.css'; - import styles from './index.scss'; + import Router from './router/index'; - const App = hot(module)(() => { - const reversedTitle = () => - setTitle( - title - .split('') - .reverse() - .join('') - ); - return ( - <div className={styles.app}> - <h1>{title}</h1> - <button type='button' onClick={reversedTitle}> - reversed title! - </button> - </div> - ); - }); + const App = hot(module)(() => ( + <div className='app'> + <Router /> + </div> + )); ReactDom.render(<App />, document.getElementById('root')); 复制代码
如今一切准备就绪,但在启动项目以前,首先说明几点
webpack
- resolve.alias
, 建立 import 或 require 的别名,来确保模块引入变得更简单。作点改进吧️ ⚓️
模块解析
<!-- starter/webpack.config.js --> ... resolve: { alias: { 'react-dom': '@hot-loader/react-dom', // react-hot-loader 兼容 hook 写法 + '@': path.resolve(__dirname, 'src'), + assets: path.resolve(__dirname, 'src/assets'), + style: path.resolve(__dirname, 'src/style') } }, ... 复制代码
管理资源
# 安装 $ yarn add -D url-loader # 将文件转换为 base64 URI。 $ yarn add -D file-loader # 将文件上的 import/require() 解析为 url,并将该文件发射到输出目录中。 复制代码
<!-- starter/webpack.config.js --> module: { rules: [ ... + { + test: /\.(png|jpe?g|gif|webp)(\?.*)?$/, // 匹配这些格式的图片 + use: [ + { + loader: 'url-loader', + options: { + limit: 4096, // 文件大小等于或大于限制,则将使用 file-loader。 + fallback: { + loader: 'file-loader', + options: { + name: 'images/[name].[hash:8].[ext]' + } + } + } + } + ] + }, + { + test: /\.(svg)(\?.*)?$/, + use: [ + { + loader: 'file-loader', + options: { + name: 'svg/[name].[hash:8].[ext]' + } + } + ] + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, + use: [ + { + loader: 'url-loader', + options: { + limit: 4096, + fallback: { + loader: 'file-loader', + options: { + name: 'fonts/[name].[hash:8].[ext]' + } + } + } + } + ] + }, + { + test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, + use: [ + { + loader: 'url-loader', + options: { + limit: 4096, + fallback: { + loader: 'file-loader', + options: { + name: 'media/[name].[hash:8].[ext]' + } + } + } + } + ] + } + ] } 复制代码
这里咱们虽然没有引入
svg
、字体图标文件、音频文件,可是这里为了方便后续深刻,咱们索性把其配置添加。
好了,启动咱们的项目。Try it!
$ yarn server
复制代码
打包
$ yarn build # 结果 $ cross-env NODE_ENV=production webpack --color --progress Hash: fb9c0cc487e7845fd915 Version: webpack 4.41.2 Time: 3533ms Built at: 2019-10-23 11:42:44 Asset Size Chunks Chunk Names chunks/vendors~main.64d1203b.js 160 KiB 1 [emitted] [immutable] vendors~main chunks/vendors~main.64d1203b.js.LICENSE 1.01 KiB [emitted] ! css/main.b7d00a9e.css 1.19 KiB 0 [emitted] [immutable] main images/logo.581fa1d8.png 8.38 KiB [emitted] index.html 667 bytes [emitted] ! main.cfa18e59.js 3.95 KiB 0 [emitted] [immutable] main Entrypoint main = chunks/vendors~main.64d1203b.js css/main.b7d00a9e.css main.cfa18e59.js 复制代码
问题与待优化点
注:使用该插件对应用进行代码分割可以帮助你“懒加载”当前用户所须要的内容,可以显著地提升你的应用性能。尽管并无减小应用总体的代码体积,但你能够避免加载用户永远不须要的代码,并在初始加载的时候减小所需加载的代码量。
安装
# 固然你也能够选择,React.lazy 和 Suspense,但他们还不支持服务端渲染。这里直接选择功能更增强大的 @loadable/component $ yarn add @loadable/component 复制代码
修改路由表
! <!-- src/router/list --> - import Github from '@/views/Github/Github'; - import Setting from '@/views/Setting/Setting'; + import React from 'react'; + import loadable from '@loadable/component'; + const Github = import(/* webpackChunkName: "github" */ '@/views/Github/Github.js'); + const Setting = import/* webpackChunkName: "setting" */ ('@/views/Setting/Setting.js'); + const AsyncComponent = (loader) => loadable(loader, { fallback: <h3>Loading...</h3> }); const routes = [ { path: '/', exact: true, redirect: '/github' }, { path: '/github', - component: Github + component: AsyncComponent(() => Github) }, { path: '/setting', - component: Setting + component: AsyncComponent(() => Setting) } ]; export default routes; 复制代码
打包咱们的应用,看一看代码分割结果
$ yarn build # 结果 $ cross-env NODE_ENV=production webpack --color --progress Hash: b93be70da668f4dff43b Version: webpack 4.41.2 Time: 6077ms Built at: 2019-10-23 16:21:08 Asset Size Chunks Chunk Names ! chunks/github.45dc6c0d.js 634 bytes 0 [emitted] [immutable] github ! chunks/setting.316d765f.js 637 bytes 2 [emitted] [immutable] setting chunks/vendors~main.a51021eb.js 164 KiB 3 [emitted] [immutable] vendors~main chunks/vendors~main.a51021eb.js.LICENSE 1.01 KiB [emitted] ! css/github.8de607a6.css 191 bytes 0 [emitted] [immutable] github ! css/setting.1a0bfbdd.css 195 bytes 2 [emitted] [immutable] setting css/main.c1fb052e.css 830 bytes 1 [emitted] [immutable] main images/logo.581fa1d8.png 8.38 KiB [emitted] index.html 667 bytes [emitted] main.02bdd0e7.js 4.96 KiB 1 [emitted] [immutable] main Entrypoint main = chunks/vendors~main.a51021eb.js css/main.c1fb052e.css main.02bdd0e7.js 复制代码
工程目录
└── starter + ├── dist + │ └── chunks + │ │ ├── github.45dc6c0d.js + │ │ ├── setting.316d765f.js + │ │ ├── vendors~main.a51021eb.js + │ │ └── vendors~main.a51021eb.js.LICENSE + ├── css + │ │ ├── 0.8de607a6.css + │ │ ├── 2.1a0bfbdd.css + │ │ └── main.c1fb052e.css + │ ├── images + │ │ └── logo.581fa1d8.png │ ├── index.html + │ └── main.02bdd0e7.js ├── node_modules ├── public │ ├── favicon.ico │ └── index.html ├── src + │ ├── assets + │ │ └── images + │ │ └── logo.png + │ ├── router + │ │ ├── index.js + │ │ └── list.js │ ├── style │ | ├── global.css │ | └── reset.css + | ├── views + │ | ├── Github + │ | │ ├── Github.js + │ | │ └── Github.scss + │ | └── Setting + │ | ├── Setting.js + │ | └── Setting.scss - │ ├── index.scss | └── index.js ├── postcss.config.js ├── webpack.config.js ├── package.json ├── README.md ├── LICENSE └── yarn.lock 复制代码
到此,咱们已经把路由功能添加,继续后续工做吧!🚘
到目前为止,咱们项目的代码量愈来愈多了,写的代码可能还会存在一些潜在问题(这很难避免);再一个,一个大型项目每每是一个团队在维护,团队成员代码风格却不尽相同。基于此,咱们须要一个工具去解决这些痛点。
工具
eslint: 经常使用于检查常见的 JavaScript 代码错误,也能够进行代码风格检查。
stylelint: 强大的现代化 linter,可帮助您避免错误并在样式中强制执行约定。
prettier: 代码格式化工具,它经过解析代码并使用本身的规则从新打印代码,从而实现一致的样式,并在必要时包装代码。
论述完编码规范的重要性,及工具链以后,咱们看看如何在项目中应用。
安装
$ yarn add -D eslint # eslint $ yarn add -D babel-eslint # 一个对 Babel 解析器的包装,使其可以与 ESLint 兼容 $ yarn add -D eslint-plugin-react # 检测 react 代码 $ yarn add -D eslint-plugin-react-hooks # 用于检测 hook 规则 $ yarn add -D eslint-plugin-jsx-a11y # 用于检测 jsx 规范 $ yarn add -D eslint-plugin-import # ESLint 插件,带有有助于验证正确导入的规则。 $ yarn add -D eslint-import-resolver-webpack # 用于 eslint-plugin-import的 Webpack-literate 模块解析插件。 复制代码
新建 eslint 配置文件
$ touch .eslintrc # eslint 配置文件 $ touch .eslintignore # eslint 忽略检测配置文件 复制代码
<!-- starter/.eslintrc -->
{
"root": true,
"env": {
"es6": true,
"browser": true,
},
"extends": ["eslint:recommended", "plugin:react/recommended", "plugin:jsx-a11y/recommended"],
"parser": "babel-eslint",
"plugins": ["react", "jsx-a11y", "react-hooks", "import"],
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "single"],
"camelcase": [0, { "properties": "never" }],
"no-console": [2, { "allow": ["warn", "error"] }],
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"react/jsx-props-no-spreading": "off",
"jsx-a11y/click-events-have-key-events": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react/no-unused-prop-types": "off"
},
"settings": {
"react": {
"version": "16.10.2"
},
"import/resolver": "webpack"
},
"globals": {
"process": true,
"module": true
}
}
<!-- starter/.eslintignore -->
node_modules
dist
复制代码
配置说明
"eslint:recommended"
启用推荐的规则"plugin:react/recommended"
该插件会导出建议的配置,以强制实施 React 的良好作法。"babel-eslint"
一个对 Babel 解析器的包装,使其可以与 ESLint 兼容eslint-plugin-import
该插件旨在支持ES2015 +(ES6 +)导入/导出语法的检查,并防止文件路径和导入名称拼写错误的问题。"import/resolver": "webpack"
:解决 webpack 别名配置致使的 eslint-plugin-import
报错。注:eslint 配置须要根据团队内部去协定出一套行之有效的规范。
修改 package.json 新建快捷命令
<!-- starter/package.json --> "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress", "build": "cross-env NODE_ENV=production webpack --color --progress", + "lint:script": "eslint --ext '.js,.jsx' src", + "lint-fix:script": "npm run lint:script -- --fix" }, 复制代码
执行命令,查看是否存在不符合规则之处
$ yarn lint:script # 执行 lint $ yarn lint-fix:script # 执行 lint 并自动修复 # 结果, 若是存在错误,则根据文档自行修复。 $ npm run lint:script -- --fix > starter@1.0.0 lint:script /Users/gt/LEMON/starter > eslint --ext '.js,.jsx' src "--fix" ✨ Done in 2.59s. 复制代码
另外,咱们但愿在每次转译js、jsx文件以前,执行 lint 格式化代码
# 安装 $ yarn add -D eslint-loader # eslint loader (for webpack) 复制代码
// 修改 webpack.config.js 配置 ... module: { rules: [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + include: path.resolve(__dirname, 'src'), + enforce: 'pre', + use: [ + { + loader: 'eslint-loader', + options: { + cache: false, + fix: true + } + } + ] + }, { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel-loader' }, ... ] } ... 复制代码
测试一下吧. Try it 🚨
安装
$ yarn add -D stylelint # 强大的现代化 linter,可帮助您避免错误并在样式中强制执行约定。 $ yarn add -D stylelint-config-recommended # Stylelint 的推荐可共享配置 $ yarn add -D postcss-reporter # 在控制台中记录 PostCSS 消息 # $ yarn add -D stylelint-config-standard # Stylelint 的标准可共享配置 # stylelint 插件经过 PostCSS 注册警告 。所以,您须要用于打印警告的 PostCSS 运行器或插件,其目的是格式化和打印警告(例如 postcss-reporter) 复制代码
新建 stylelint 配置文件
$ touch .stylelintrc # stylelint 配置文件 复制代码
<!-- starter/.eslintrc -->
# 你也可使用 stylint 推荐开启的规则, 只需引入扩展推荐包便可。
# 你也能够 使用 rules 扩充规则或者覆盖推荐规则,这取决于你!
{
"extends": "stylelint-config-recommended",
"rules": {
"indentation": 2, // 缩进
"declaration-colon-space-after": "always", // 在冒号声明后须要一个空格或禁止使用空格。 a { color:pink } => a { color: pink }
"declaration-colon-space-before": "never", // 在冒号以前须要一个空格或禁止空格。 a { color : pink } => a { color: pink }
"function-comma-space-after": "always", // 在功能的逗号后面须要一个空格或不容许空格。 a { transform: translate(1,1) } => a { transform: translate(1, 1) }
"function-url-quotes": "always", // 要求或禁止使用网址引号 a { background: url(x.jpg) } => a { background: url("x.jpg") }
"media-feature-colon-space-before": "never", // 媒体功能中的冒号以前须要单个空格或不容许使用空格。@media (max-width :600px) {} => @media (max-width:600px) {}
"media-feature-name-no-vendor-prefix": true, // 禁止使用媒体功能名称的供应商前缀。@media (-webkit-min-device-pixel-ratio: 1) {} => @media (min-resolution: 96dpi) {}
"max-empty-lines": 5, // 限制相邻的空行数。
"number-leading-zero": "never", // 小数部分小于或等于1的前导零。a { line-height: 0.5; } => a { line-height: .5; }
"number-no-trailing-zeros": true, // 禁止数字尾随零。a { top: 1.0px } => a { top: 1px }
"at-rule-semicolon-newline-after": "always", // 规则后的分号换行符 @import url("x.css"); a {} => @import url("x.css");\n a {}
"selector-list-comma-space-before": "never", // 选择器列表的逗号前须要一个空格或不容许空格 a ,b { color: pink; } => a, b { color: pink; }
"selector-list-comma-newline-after": "always", // 选择器列表的逗号后须要换行符或不容许使用空格。a, b { color: pink; } => a,\n b { color: pink; }
"string-quotes": "single", // 在字符串周围指定单引号或双引号。 a { content: “x”; } => a { content: 'x'; }
}
}
复制代码
扩展共享配置及规则表
添加快捷命令
<!-- starter/package.json --> "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress", "build": "cross-env NODE_ENV=production webpack --color --progress", "lint:script": "eslint --ext '.js,.jsx' src", "lint-fix:script": "npm run lint:script -- --fix", + "lint:style": "stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss", + "lint-fix:style": " npm run lint:style -- --fix", }, 复制代码
配置 postcss-reporter
在控制台中记录 PostCSS 消息
<!-- starter/postcss.config.js --> module.exports = { plugins: { autoprefixer: {}, + 'postcss-reporter': { + clearReportedMessages: true, # 插件将在记录结果消息后清除它们。这样能够防止其余插件或您使用的任何运行程序再次记录相同的信息并引发混乱。 + throwError: true # 在插件记录您的消息后,若是发现任何警告,它将引起错误。 + }, } }; 复制代码
执行命令,查看是否存在不符合规则之处
$ yarn lint:style # 格式化 style $ yarn lint-fix:style # 格式化 style 并自动修复 # 结果 $ npm run lint:style -- --fix > starter@1.0.0 lint:style /Users/gt/LEMON/starter > stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss "--fix" src/style/reset.css 54:1 ✖ Expected selector "h1" to come before selector "h1:first-child" no-descending-specificity 54:1 ✖ Expected selector "h1" to come before selector "h1:last-child" no-descending-specificity 58:1 ✖ Expected selector "h2" to come before selector "h2:first-child" no-descending-specificity 58:1 ✖ Expected selector "h2" to come before selector "h2:last-child" no-descending-specificity 62:1 ✖ Expected selector "h3" to come before selector "h3:first-child" no-descending-specificity 62:1 ✖ Expected selector "h3" to come before selector "h3:last-child" no-descending-specificity 66:1 ✖ Expected selector "h4" to come before selector "h4:first-child" no-descending-specificity 66:1 ✖ Expected selector "h4" to come before selector "h4:last-child" no-descending-specificity 67:1 ✖ Expected selector "h5" to come before selector "h5:first-child" no-descending-specificity 67:1 ✖ Expected selector "h5" to come before selector "h5:last-child" no-descending-specificity 68:1 ✖ Expected selector "h6" to come before selector "h6:first-child" no-descending-specificity 68:1 ✖ Expected selector "h6" to come before selector "h6:last-child" no-descending-specificity # no-descending-specificity 禁止较低特异性的选择器在覆盖较高特异性的选择器以后出现。 # 根据规则表修复 reset.css 文件 # 再次运行,结果: $ npm run lint:style -- --fix > starter@1.0.0 lint:style /Users/gt/LEMON/starter > stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss "--fix" ✨ Done in 1.96s. 复制代码
测试一下吧. Try it 💄
安装
$ yarn add -D prettier $ yarn add -D eslint-plugin-prettier # 将 Prettier 做为 ESLint 规则运行,并将差别报告为单个ESLint问题 $ yarn add -D eslint-config-prettier # 关闭全部没必要要的或可能与 Prettier 冲突的规则。 $ yarn add -D stylelint-config-prettier # 禁用与 Prettier 冲突的规则的配置 复制代码
关于这些禁用规则,请参考 eslint-config-prettier#special-rules, stylelint-config-prettier special-rules
在 eslint 配置中扩展 prettier
<!-- starter/.eslintrc --> { ... - "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:jsx-a11y/recommended"], + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:jsx-a11y/recommended", + "plugin:prettier/recommended", + "prettier/react" + ], ... } <!-- 说明 --> "plugin:prettier/recommended" does three things: 1. Enables eslint-plugin-prettier. 2. Sets the prettier/prettier rule to "error". 3. Extends the eslint-config-prettier configuration. "prettier/react" 为了支持特殊的 ESLint 插件(eslint-plugin-react)所添加额外的排除项 复制代码
固然,你能够在
.prettierrc
文件中设置Prettier
本身的选项。
新建 prettier 配置文件
$ touch .prettierrc # prettier 配置文件 复制代码
<!-- starter/.prettierrc -->
{
"semi": true,
"singleQuote": true,
"trailingComma": 'all',
}
复制代码
在 stylelint 配置中扩展 prettier
<!-- starter/.stylelintrc --> { ... - "extends": "stylelint-config-recommended", + "extends": [ + "stylelint-config-recommended", + "stylelint-config-prettier" + ], ... } 复制代码
说明
添加快捷命令行
<!-- starter/package.json --> ... "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress", "build": "cross-env NODE_ENV=production webpack --color --progress", "lint:script": "eslint --ext '.js,.jsx' src", "lint-fix:script": "npm run lint:script -- --fix", "lint:style": "stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss", "lint-fix:style": " npm run lint:style -- --fix", + "prettier": "prettier --check --write 'src/**/*.{js,jsx,scss,css}' --config ./.prettierrc" }, ... 复制代码
更多参数请参考 Prettier CLI
运行命令,格式化代码
$ yarn prettier # 结果, 它帮你格式化的代码以下 $ prettier --check --write './src/**/*.js' './src/**/*.jsx' Checking formatting... src/index.js src/router/index.js src/router/list.js src/views/Github/Github.js src/views/Setting/Setting.js Code style issues fixed in the above file(s). ✨ Done in 0.79s. 复制代码
Git钩子脚本对于在提交代码审查以前识别简单问题颇有用。咱们在每次提交时都运行钩子,以自动指出代码中的问题,例如缺乏分号,尾随空白和调试语句。经过在代码审阅以前指出这些问题,一来,能够确保没有错误进入存储库;二来,代码审阅者能够专一于更改的体系结构,而不会由于琐碎的风格问题而浪费时间。
安装
$ yarn add -D husky # 🐶 Git hooks made easy $ yarn add -D lint-staged # 对暂存的 git 文件运行 linters,不要让💩进入您的代码库! 复制代码
配置
<!-- starter/package.json --> { "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress", "build": "cross-env NODE_ENV=production webpack --color --progress", + "lint": "npm run lint:style && npm run lint:script", + "lint-fix": "npm run lint-fix:style && npm run lint-fix:script", "lint:script": "eslint --ext '.js,.jsx' src", "lint-fix:script": "npm run lint:script -- --fix", "lint:style": "stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss", "lint-fix:style": " npm run lint:style -- --fix", "prettier": "prettier --check --write 'src/**/*.{js,jsx,scss,css}' --config ./.prettierrc" }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{js, jsx, css, scss}": [ + "npm run prettier", + "npm run lint-fix", + "git add" + ] + } } 复制代码
推个代码测试一下吧! Try it! 🎊🎊
题外话:commit changelog 规范
# feat: 添加新功能(feature) # fix : 修复 bug # docs: 文档(documentation) # style: 样式及代码格式化等不涉及逻辑的改动点 # refactor: 重构 # test: 添加测试用例 # chore: 构建过程或辅助工具的变更 # 这里推荐一个 lint 插件 commitlint。可根据须要添加 # 详细参考:https://github.com/conventional-changelog/commitlint # 关于 commit 信息编写的更多规范指南 # 请参考:http://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html 复制代码
到此编码规范的内容基本陈述完毕,说的东西有限、具体如何配置取决于你或你的团队要求! Go 🚠
为了接下来更好的论述,咱们来完成一个小需求。
由草图需求改造咱们的项目
改造路由表
import React from 'react'; import loadable from '@loadable/component'; import Loading from '@/components/Loading/Loading'; const BottomTabNavigator = import( /* webpackChunkName: "bottom-tab-navigator" */ '@/components/BottomTabNavigator/BottomTabNavigator' ); const Empty = import( /* webpackChunkName: 'not-found' */ '@/components/Empty/Empty' ); const Github = import(/* webpackChunkName: "github" */ '@/views/Github/Github'); const Setting = import( /* webpackChunkName: "setting" */ '@/views/Setting/Setting' ); const AsyncComponent = loader => loadable(loader, { fallback: <Loading /> }); const routes = [ { path: '/', exact: true, redirect: '/dashboard/github', }, { path: '/dashboard', component: AsyncComponent(() => BottomTabNavigator), routes: [ { path: '/dashboard/github', component: AsyncComponent(() => Github), }, { path: '/dashboard/setting', component: AsyncComponent(() => Setting), }, ], }, { path: '*', component: AsyncComponent(() => Empty), }, ]; export default routes; 复制代码
改造 Github 页面
/* * 路径: starter/src/views/Github * 说明: * RepositoriesCard 根据草图编写的仓库信息卡片 * Loading 加载态组件 * Empty 空数据态组件 * useRequest 自定义 hook,用于包装请求 * searchRepositories 统一 API 请求封装 * * 提示: 说明涉及到的组件,能够参考项目;你也能够本身实现,这不重要。 */ import React from 'react'; import styles from './Github.scss'; import RepositoriesCard from '@/components/RepositoriesCard/RepositoriesCard'; import Loading from '@components/Loading/Loading'; import Empty from '@components/Empty/Empty'; import useRequest from '@/containers/useRequest'; import { searchRepositories } from '@/services/api/github'; function Github() { const [loading, data] = useRequest(searchRepositories, { q: 'javascript' }); if (loading === true) { return <Loading /> } return ( <div className={styles.root}> {(data && data.items.map( ({ description, id, name, forks_count, stargazers_count, language, owner }) => ( <RepositoriesCard key={id} name={name} avatarUrl={owner.avatar_url} description={description} stargazersCount={stargazers_count} forksCount={forks_count} language={language} /> ) )) || <Empty />} </div> ); } export default Github; 复制代码
改造 Setting 页面(不改造 😜)
咱们在改造 Github 页面, 在组件内部调用了请求方法,并对请求作了统一封装,在继续改造工做以前,咱们先来看看 先后端交互
安装
$ yarn add axios # Promise based HTTP client for the browser and node.js 复制代码
新建相关文件
# 新建 services 文件夹 $ cd src && mkdir services $ cd services && touch index.js # 基于 axios 简单封装 $ mkdir interface && cd interface # 用于存在项目全部接口 $ touch github.js # 用于存放 GitHub 相关请求 复制代码
基于 axios 简单封装 src/services/index.js
/** * 说明: AXIOS_DEFAULT_OPTIONS 默认配置,详细参考 utils * * 注: 如下封装仅仅简单包装一层,你也能够本身实现。 */ import axios from 'axios'; import constants from '@/utils/constants'; // 使用自定义配置新建一个 axios 实例 const instance = axios.create(constants.AXIOS_DEFAULT_OPTIONS); // 请求拦截器 instance.interceptors.request.use( (AxiosRequsetConfig) => AxiosRequsetConfig, // 在发送请求以前作些什么 (error) => Promise.reject(error) // 对请求错误作些什么 ); // 响应拦截器 instance.interceptors.response.use( (AxiosResponse) => AxiosResponse, // 对响应数据作点什么 (error) => Promise.reject(error) // 对响应错误作点什么, 如,处理一些鉴权类问题 ); export default function (options = {}, customConfig = {}) { return new Promise((resolve, reject) => { const finalConfig = Object.assign(options, customConfig); instance(finalConfig) .then(({ data }) => { if (data) { return resolve(data); } return reject(new Error('Request return result exception!')); }) .catch((reason) => reject(reason)); }); } 复制代码
业务接口层 src/services/interface/github.js
import network from '../index'; /** * @desc 搜索仓库 * * @param {Object} data 请求参数 * @returns {Promise} */ export const searchRepositories = (data = {}) => network({ url: '/search/repositories', params: data }); 复制代码
上述简单封装核心请求方法,分离接口等,主要目的是辅助论述,固然,这还很简单,你能够本身根据实际须要作更全面的封装!
准则
样例
import React from 'react'; import PropTypes from 'prop-types'; const UI = ({ title }) => { return ( <div className="UI"> { title } </div> ); }; UI.propTypes = { title: PropTypes.string, }; UI.defaultProps = { title: 'UI Component !', }; export default UI; 复制代码
准则
样例
import { connect } from 'react-redux'; import Demo from 'components/Demo/Demo'; import { incrementEnthusiasm, decrementEnthusiasm } from 'actions/index'; export function mapStateToProps({ enthusiasm }) { return { enthusiasm, }; } export function mapDispatchToProps(dispatch) { return { onIncrement: () => dispatch(actions.incrementEnthusiasm()), onDecrement: () => dispatch(actions.decrementEnthusiasm()), }; } export default connect(mapStateToProps, mapDispatchToProps)(Demo); 复制代码
Tip: 因为对 react 不是很熟,故谈的比较简单,这里推荐参考:Presentational and Container Components、编写有弹性的组件
这里咱们直接引入
postcss-px-to-viewport
插件。
安装
$ yarn add -D postcss-px-to-viewport`
复制代码
配置
<!-- starter/postcss-config.js --> module.exports = { plugins: { autoprefixer: {} }, 'postcss-reporter': { clearReportedMessages: true, throwError: true }, + 'postcss-px-to-viewport': { + viewportWidth: 375, // 设计稿的视口宽度 + viewportHeight: 812, // 设计稿的视口高度 + unitPrecision: 5, // 单位转换后保留的精度 + viewportUnit: 'vw', // 但愿使用的视口单位 + fontViewportUnit: 'vw', // 字体使用的视口单位 + selectorBlackList: ['.ignore', '.hairlines'], // 须要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。 + minPixelValue: 1, // 设置最小的转换数值,若是为1的话,只有大于1的值会被转换 + mediaQuery: false, // 媒体查询里的单位是否须要转换单位 + exclude: [/node_modules/] // 须要排除的 + } }; 复制代码
运行项目,看看效果!
$ yarn server # 运行项目 # 结果 $ cross-env NODE_ENV=development webpack-dev-server --color --progress 10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://localhost:3000/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public ℹ 「wds」: 404s will fallback to /index.html ℹ 「wdm」: Compiled successfully. 复制代码
🔥 Good job!🎉 🔥
└── starter
├── dist
│ ├── chunks
│ │ ├── bottom-tab-navigator.77d17027.js
│ │ ├── github.4e7f6c35.js
│ │ ├── not-found.638dbdfc.js
│ │ ├── setting.bc3fbe14.js
│ │ ├── vendors~github.7acdaa67.js
│ │ ├── vendors~github.7acdaa67.js.LICENSE
│ │ ├── vendors~main.b1a4bdbf.js
│ │ └── vendors~main.b1a4bdbf.js.LICENSE
│ ├── css
│ │ ├── bottom-tab-navigator.25c0dead.css
│ │ ├── github.866c72ba.css
│ │ ├── main.45091b7c.css
│ │ ├── not-found.d566b1be.css
│ │ └── setting.2b60ef7c.css
│ ├── fonts
│ │ ├── iconfont.63765329.woff
│ │ ├── iconfont.c2eabadd.ttf
│ │ └── iconfont.cad7bb52.eot
│ ├── images
│ │ ├── empty-data.788c1924.png
│ │ ├── logo.581fa1d8.png
│ │ └── webpage-lost.a02f7942.png
│ ├── svg
│ │ └── iconfont.1247822e.svg
│ ├── main.a010b425.js
│ └── index.html
├── node_modules
| ├── ...
| └── ...
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── assets
│ │ ├── font
│ │ │ ├── iconfont.css
│ │ │ ├── iconfont.eot
│ │ │ ├── iconfont.svg
│ │ │ ├── iconfont.ttf
│ │ │ └── iconfont.woff
│ │ └── images
│ │ ├── empty-data.png
│ │ ├── logo.png
│ │ └── webpage-lost.png
│ ├── components
│ │ ├── BottomTabNavigator
│ │ │ ├── BottomTabNavigator.js
│ │ │ ├── BottomTabNavigator.scss
│ │ │ └── index.zh-CN.md
│ │ ├── Circle
│ │ │ ├── Circle.js
│ │ │ ├── Circle.scss
│ │ │ └── index.zh-CN.md
│ │ ├── Empty
│ │ │ ├── Empty.js
│ │ │ ├── Empty.scss
│ │ │ └── index.zh-CN.md
│ │ ├── Loading
│ │ │ ├── Loading.js
│ │ │ ├── Loading.scss
│ │ │ └── index.zh-CN.md
│ │ ├── README.md
│ │ └── RepositoriesCard
│ │ ├── RepositoriesCard.js
│ │ ├── RepositoriesCard.scss
│ │ └── index.zh-CN.md
│ ├── containers
│ │ ├── README.md
│ │ └── useRequest.js
│ ├── index.js
│ ├── router
│ │ ├── index.js
│ │ └── list.js
│ ├── services
│ │ ├── index.js
│ │ └── interface
│ │ └── github.js
│ ├── style
│ │ ├── global.css
│ │ ├── reset.css
│ │ └── variable.scss
│ ├── utils
│ │ ├── constants.js
│ │ ├── enume.js
│ │ └── tools.js
│ └── views
│ ├── Github
│ │ ├── Github.js
│ │ └── Github.scss
│ └── Setting
│ ├── Setting.js
│ └── Setting.scss
├── webpack.config.js
├── postcss.config.js
├── package.json
├── LICENSE
├── README.md
└── yarn.lock
复制代码
项目改造到此已基本完成,但后续仍然还有工做要作 💊😯。继续吧!
先后端分离,让前端脱离后台独立开发,mock 起了很大的做用。在实际业务开发中,咱们须要一种能不侵入现有代码,便可拦截请求,返回模拟数据。 咱们利用 json-server 帮助咱们完成这个需求。
安装
$ yarn add -D json-server
复制代码
新建 mock 文件夹
$ mkdir mock $ cd mock && touch index.js $ mkdir interface && cd interface $ touch index.js && touch github.js 复制代码
// starter/mock/index.js const data = require('./interface/index'); module.exports = function Mock() { return data; }; // starter/mock/interface/index.js const github = require('./github'); module.exports = { ...github, }; // starter/mock/interface/github.js const repositories = { "items": [ { "id": 6498492, "name": "javascript", "full_name": "airbnb/javascript", "owner": { "login": "airbnb", "id": 698437, "avatar_url": "https://avatars3.githubusercontent.com/u/698437?v=4", }, "description": "JavaScript Style Guide", "size": 3002, "stargazers_count": 89966, "watchers_count": 89966, "language": "JavaScript", "forks_count": 17404, "open_issues_count": 110, "license": { "key": "mit", "name": "MIT License", }, "forks": 17404, "open_issues": 110, "watchers": 89966, "default_branch": "master", "score": 151.055 }, { "id": 18286232, "name": "javascript", "full_name": "GitbookIO/javascript", "private": false, "owner": { "login": "GitbookIO", "id": 7111340, "avatar_url": "https://avatars0.githubusercontent.com/u/7111340?v=4", }, "description": "GitBook teaching programming basics with Javascript", "size": 1267, "stargazers_count": 1923, "watchers_count": 1923, "language": "javascript", "forks_count": 730, "open_issues_count": 43, "license": { "key": "apache-2.0", "name": "Apache License 2.0", }, "forks": 730, "open_issues": 43, "watchers": 1923, "default_branch": "master", "score": 104.4313 }, ] } module.exports = { repositories }; 复制代码
数据来源于 GitHub ,这里只作演示,故直接贴出数据。若是你须要动态生成数据,能够引入 mockjs 帮助你生成数据。这里就不作赘述了!
配置快捷命令 starter/package.json
... { "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress", + "server:mock": "npm run mock & cross-env NODE_ENV=development MOCK=true webpack-dev-server --color --progress", + "mock": "json-server mock/index.js --watch --port 3001", "build": "cross-env NODE_ENV=production webpack --color --progress", "lint": "npm run lint:style && npm run lint:script", "lint-fix": "npm run lint-fix:style && npm run lint-fix:script", "lint:script": "eslint --ext '.js,.jsx' src", "lint-fix:script": "npm run lint:script -- --fix", "lint:style": "stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss", "lint-fix:style": " npm run lint:style -- --fix", "prettier": "prettier --check --write 'src/**/*.{js,jsx,scss,css}' --config ./.prettierrc" }, } ... 复制代码
配置 dev-server 代理
# 新建代理文件 $ cd ../.. && mkdir config $ cd config && touch proxy.js 复制代码
// starter/config/proxy.js /** * @desc mock 服务代理配置 */ const MOCK_SERVER_PROXY = { '/search/*': { target: 'http://localhost:3001/$1', } } /** * @desc 默认服务代理 */ const DEFAULT_PROXY = {}; /** * @desc dev-server 代理配置 * @param {Boolean} IS_MOCK mock 标识 * @param {Object} Proxy */ module.exports = function({ IS_MOCK }) { if (IS_MOCK) return MOCK_SERVER_PROXY; return DEFAULT_PROXY; } 复制代码
具体如何配置代理,根据接口自定!更多请参考
devServer - proxy
# starter/webpack.config.js ... + const IS_MOCK = process.env.MOCK === 'true'; + const filterProxy = require('./config/proxy'); ... baseConfig.devServer = { ... + proxy: filterProxy({ IS_MOCK }) } ... 复制代码
运行项目
$ yarn server:mock # 结果 $ npm run mock & cross-env NODE_ENV=development MOCK=true webpack-dev-server --color --progress > starter@1.0.0 mock /Users/mr.lemon/cl/CODE_CL/REACT/starter > json-server mock/index.js --watch --port 3001 \{^_^}/ hi! Loading mock/index.js Done Resources http://localhost:3001/repositories Home http://localhost:3001 Type s + enter at any time to create a snapshot of the database Watching... 10% building 1/1 modules 0 activeℹ 「wds」: Project is running at http://192.168.0.102:3000/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from /Users/mr.lemon/cl/CODE_CL/REACT/starter/public ℹ 「wds」: 404s will fallback to /index.html ℹ 「wdm」: Compiled successfully. 复制代码
以上仅仅阐述了 mock 这一环, 关于先后端分离这里推荐一个知乎问答 Web 先后端分离的意义大吗?
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工做。业内优秀的测试框架不少,这里直接选择 jest。
安装
$ yarn add -D jest # Jest is a delightful JavaScript Testing Framework with a focus on simplicity. $ yarn add -D babel-jest # Jest plugin to use babel for transformation $ yarn add -D enzyme # 一种用于 React 的 JavaScript 测试实用程序,能够更轻松地测试 React 组件的输出。您还能够操纵,遍历并以某种方式模拟给定输出的运行时。 $ yarn add -D enzyme-adapter-react-16 # react 16 适配器 $ yarn add -D identity-obj-proxy # 模拟一个代理以启用 className 查找 复制代码
新建用于存放测试用例的文件夹及 jest 配置文件
$ touch jest.config.js $ cd src && mkdir __tests__ $ cd __tests__ $ mkdir __mocks__ && mkdir ui && touch setup.js $ cd __mocks__ && touch fileMock.js $ cd ../ui && touch Loading.spec.js 复制代码
配置 jest
<!-- starter/jest.config.js --> module.exports = { testRegex: '(\\.)(test|spec)(\\.)jsx?$', // 处理静态文件 // 样式表和图像等,这些文件在测试中无足轻重,由于咱们能够安全地 mock 他们。 // 模拟 CSS 模块,用类名查找模拟一个代理 moduleNameMapper: { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/src/__tests__/__mocks__/fileMock.js', '\\.(css|scss|sass)$': 'identity-obj-proxy', '^@/(.*)$': '<rootDir>/src/$1' }, // 为转换源文件提供同步功能的模块 transform: { '^.+\\.(js|jsx)$': 'babel-jest' }, // 在每次测试以前配置或设置测试环境 setupFiles: ['<rootDir>/src/__tests__/setupTests.js'] }; <!-- starter/src/__tests__/__mocks__/fileMock.js --> module.exports = 'test-file-stub'; 复制代码
注册 enzyme 适配器配置
// starter/src/__tests__/setup.js import enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; enzyme.configure({ adapter: new Adapter() }); 复制代码
配置快捷运行命令
<!-- starter/package.json --> { ... "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "jest --config jest.config.js --no-cache", "server": "cross-env NODE_ENV=development webpack-dev-server --color --progress", "server:mock": "npm run mock & cross-env NODE_ENV=development MOCK=true webpack-dev-server --color --progress", "mock": "json-server mock/index.js --watch --port 3001", "build": "cross-env NODE_ENV=production webpack --color --progress", "lint": "npm run lint:style && npm run lint:script", "lint-fix": "npm run lint-fix:style && npm run lint-fix:script", "lint:script": "eslint --ext '.js,.jsx' src", "lint-fix:script": "npm run lint:script -- --fix", "lint:style": "stylelint 'src/**/*.css' 'src/**/*.scss' --syntax scss", "lint-fix:style": " npm run lint:style -- --fix", "prettier": "prettier --check --write 'src/**/*.{js,jsx,scss,css}' --config ./.prettierrc" }, ... } 复制代码
编写测试用例
import React from 'react'; import { shallow } from 'enzyme'; import Loading from '../../components/Loading/Loading'; describe('Loading 组件基础测试组合!', () => { it('<Loading /> 组件默认标题应该是 "loading..."', () => { const loading = shallow(<Loading />); expect(loading.find('span').text()).toBe('loading...'); }); it('<Loading /> 组件标题应该是 "加载中..."', () => { const loading = shallow(<Loading title='加载中...' />); expect(loading.find('span').text()).toBe('加载中...'); }); }); 复制代码
这里的用例只作演示,在实际开发中要严格根据 UI 组件的功能编写用例。
运行测试
$ yarn test # 结果 $ jest --config jest.config.js --no-cache PASS src/__tests__/ui/Loading.spec.js Loading 组件基础测试组合! ✓ <Loading /> 组件默认标题应该是 "loading..." (7ms) ✓ <Loading /> 组件标题应该是 "加载中..." (1ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 1.66s Ran all test suites. ✨ Done in 2.44s. 复制代码
说明
try it! 🍁
在实际项目中,考虑到前端性能(首屏加载,全屏加载,白屏时间)都会对打出的资源包进行分析,而后采起相应的方案进行优化。咱们在前面论述打包构建时,已经在多个方面进行说明了,这里不在赘述。
# 安装 $ yarn add -D webpack-bundle-analyzer # 交互式可缩放树图可视化webpack输出文件的大小。 复制代码
# 添加相应配置 starter/webpack.config.js ... const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); if (IS_PROD) { ... + baseConfig.plugins.push( + new BundleAnalyzerPlugin() + ); } ... 复制代码
$ yarn build # 结果 $ cross-env NODE_ENV=production webpack --color --progress 98% after emitting SizeLimitsPluginWebpack Bundle Analyzer is started at http://127.0.0.1:8888 Use Ctrl+C to close it Hash: 3bf5742858f4d53ed50d Version: webpack 4.41.2 Time: 3079ms Built at: 2019-10-27 21:43:54 Asset Size Chunks Chunk Names chunks/bottom-tab-navigator.04852ffb.js 1.54 KiB 0 [emitted] [immutable] bottom-tab-navigator chunks/github.a1f67f6e.js 4.65 KiB 1, 3 [emitted] [immutable] github chunks/not-found.b1d86988.js 576 bytes 3 [emitted] [immutable] not-found chunks/setting.34d43c5a.js 673 bytes 4 [emitted] [immutable] setting chunks/vendors~github.d521d263.js 24.5 KiB 5 [emitted] [immutable] vendors~github chunks/vendors~github.d521d263.js.LICENSE 120 bytes [emitted] chunks/vendors~main.89c9b5d2.js 166 KiB 6 [emitted] [immutable] vendors~main chunks/vendors~main.89c9b5d2.js.LICENSE 1.01 KiB [emitted] css/bottom-tab-navigator.b9b2f600.css 1.54 KiB 0 [emitted] [immutable] bottom-tab-navigator css/github.e0893952.css 1.99 KiB 1, 3 [emitted] [immutable] github css/main.09cf98ab.css 11 KiB 2 [emitted] [immutable] main css/not-found.091bbea2.css 64 bytes 3 [emitted] [immutable] not-found css/setting.42e9f58f.css 252 bytes 4 [emitted] [immutable] setting fonts/iconfont.63765329.woff 4.58 KiB [emitted] fonts/iconfont.c2eabadd.ttf 7.52 KiB [emitted] fonts/iconfont.cad7bb52.eot 7.69 KiB [emitted] images/empty-data.788c1924.png 11.7 KiB [emitted] images/logo.581fa1d8.png 8.38 KiB [emitted] images/webpage-lost.a02f7942.png 13.5 KiB [emitted] index.html 667 bytes [emitted] main.02466eca.js 6.64 KiB 2 [emitted] [immutable] main svg/iconfont.1247822e.svg 22 KiB [emitted] Entrypoint main = chunks/vendors~main.89c9b5d2.js css/main.09cf98ab.css main.02466eca.js 复制代码
在实际开发环境中,公司内部都会有一套持续集成的东西帮助咱们去部署。这里为了演示方便,咱们采用 Travis CI 帮助咱们去作这个事情。
Travis CI: 代码有变动,自动运行构建和测试,并反馈运行结果。
如何使用配置?请参考 持续集成服务 Travis CI 教程
本项目的 yml 脚本,请参考项目
预览 「preview」