这个脚手架适用以下这种项目:
- 使用React作为构建用户界面的库。
- 使用TypeScript进行类型检查。
- 使用React Router进行路由管理。
- 使用Redux进行状态管理。
- 使用Sass编写样式。
- 使用Eslint保持代码风格的一致。
- 使用Jest等进行单元测试。
打包代码的工具使用了Webpack,并且用Bable将JavaScript编译为浏览器兼容的版本。
以下文章:
《使用Webpack等搭建一个适用于React项目的脚手架(1 - React、TypeScript)》
《使用Webpack等搭建一个适用于React项目的脚手架(2 - React Router、Redux、Sass)》
《使用Webpack等搭建一个适用于React项目的脚手架(3 - Eslint、Jest)》
《使用Webpack等搭建一个适用于React项目的脚手架(4 - 优化)》
记录使用Webpack等搭建一个适用于React项目的开发环境。
《使用Webpack等搭建一个适用于React项目的脚手架(5 - 脚手架)》中记录搭建一个脚手架,脚手架的功能是使用指令获取前几篇文章中写好的代码,创建一个项目。
初始化
创建一个文件夹并进入文件目录下:
mkdir simple-scaffold && cd simple-scaffold
初始化项目:
npm init -y
-y 的意思是初始化项目的过程中所有的选项选择默认项,执行完命令后文件目录下多了一个package.json文件,这个文件目前是这样的(以下注释只是说明用,json文件中不存在):
{
"name": "simple-scaffold", // 项目名称,默认取所在文件夹的名称
"version": "1.0.0", // 版本号,发布包的时候会用到
"description": "", // 描述,在npm搜索中会用到
"main": "index.js", // 程序的主入口,假如发布了包用户又require了这个包,那么require返回的内容就是index.js中导出的内容
"scripts": { // scripts中的内容是配置的脚本命令
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [], // 程序的关键字,在npm搜索中会用到
"author": "",
"license": "ISC"
}
创建一个src文件夹,并在src文件夹下创建index.js和index.html文件:
mkdir src && touch src/index.js src/index.html
index.js 的文件内容如下:
window.onload = function () {
var root = document.getElementById('root');
var content = document.createElement('h1');
content.textContent = '使用Webpack等搭建一个适用于React项目的脚手架';
root.appendChild(content);
}
Index.html的文件内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>simple-scaffold</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Webpack配置
在项目根目录执行以下语句,安装好webpack和webpack-cli:
npm i --save-dev webpack webpack-cli
创建一个文件夹并进入文件目录下:
mkdir config && cd config
在config文件夹下创建三个文件,分别用于放置通用的webpack配置,开发环境的webpack配置以及生产环境的webpack配置:
touch webpack.common.js webpack.dev.js webpack.prod.js
打包html、js文件
安装html-loader、html-webpack-plugin、clean-webpack-plugin:
npm i --save-dev html-webpack-plugin html-loader clean-webpack-plugin
webpack.common.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'none',
entry: {
app: path.resolve(__dirname, '../src/index.js'),
},
output: {
filename: '[name].[hash].js',
path: path.resolve(__dirname, '../dist'),
publicPath: './',
},
module: {
rules: [
{
test: /\.html$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'html-loader',
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'simple-scaffold',
template: path.resolve(__dirname, '../src/index.html'),
filename: 'index.html',
}),
],
}
mode指webpack打包的模式,webpack会根据不同的配置模式进行相应的优化。代码中的 mode: 'none'表示不使用任何优化。
entry指定打包的入口,webpack会从entry指定的文件开始,生成一个依赖关系图。当entry以对象的方式定义的时候,键值就是输出文件的name。
output中filename定义了输出的文件名,上述代码[name].[hash].js中的hash是模块标志符的hash值。publicPath指定生成的文件的公共路径,比如以上代码生成的html文件中,引入的js的路径为src="./app.17934a47c82529729b11.js"。如果把publicPath: './' 改为publicPath: '/test/' ,那么生成的js文件的引入路径为src="/test/app.17934a47c82529729b11.js"。
webpack在不配置loader的情况下只能打包JavaScript文件,使用loader之后能处理各式各样的文件(modules)。上述代码中的html-loader就是专门用来将html文件解析为字符串的,html-webpack-plugin用于生成一个html文件。clean-webpack-plugin用于清除上次打包的文件。
在package.json中添加:
"scripts": {
"build": "webpack --config ./config/webpack.common.js"
},
这样当执行npm run build的时候,就相当于执行webpack --config ./config/webpack.common.js,--config指定webpack的配置文件。
执行npm run build打包完文件之后,查看打包好的app.17934a47c82529729b11.js文件,发现里面已经包含了入口文件src/index.js中的代码。
/***/ (function(module, exports) {
window.onload = function () {
var root = document.getElementById('root');
var content = document.createElement('h1');
content.textContent = '使用Webpack等搭建一个适用于React项目的脚手架';
root.appendChild(content);
}
/***/ })
打包后的html文件中引入了打包好的js文件:
...
<script type="text/javascript" src="./app.17934a47c82529729b11.js"></script></body>
</html>
在浏览器中打开生成的html文件能看见页面内容为“使用Webpack等搭建一个适用于React项目的脚手架”。
区分开发/生产环境
上文中打包好的文件中,打包后的js文件和html文件都是没有压缩的,但是生产环境需要保证代码体积尽量小,所以在生产环境需要压缩代码。开发环境一般会使用devServer来配置webpack-dev-server,webpack-dev-server提供了一个服务器,可以在本地服务器上访问打包好的文件(webpack-dev-server将文件内容放在了内存中,并没有将内容写(write)成文件),而生产环境不需要webpack-dev-server。打包后的文件如果在执行中报错了,只能在打包后文件(app.17934a47c82529729b11.js)中定位到错误的位置,不能定位到源码(src/index.js),所以开发环境一定需要使用devtool或者别的方式(比如SourceMapDevToolPlugin)来将打包后代码映射到打包前的代码,源码映射(source map)在生产环境是可有可无的。总之,开发环境和生产环境有很多不同,所以需要分别配置。
安装cross-env:
npm i --save-dev cross-env
修改package.json文件的scripts部分:
"scripts": {
"start": "cross-env NODE_ENV=development webpack-dev-server --config ./config/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.js",
"build:dev": "cross-env NODE_ENV=development webpack --config ./config/webpack.dev.js"
},
设置build:dev是为了方便查看以不压缩的方式打包后的代码内容。
cross-env是用于跨平台设置和使用环境变量的,不同操作系统设置环境变量的方式不一定相同,比如Mac电脑上使用export NODE_ENV=development,而Windows电脑上使用的是set NODE_ENV=development,使用cross-env NODE_ENV=development时不论在Windows电脑上还是Mac电脑上,都能成功设置环境变量NODE_ENV为development。
安装webpack-merge、webpack-dev-server,webpack-merge用来合并webpack配置, webpack-dev-server提供一个服务器。
npm i --save-dev webpack-merge webpack-dev-server
稍微修改一下webpack.common.js、 webpack.dev.js、webpack.prod.js三个文件中的内容:
webpack.common.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const devMode = process.env.NODE_ENV !== 'production';
module.exports = {
entry: {
app: path.resolve(__dirname, '../src/index.js'),
},
output: {
filename: devMode ? '[name].js' : '[name].[hash].js',
path: path.resolve(__dirname, '../dist'),
publicPath: '/',
},
module: {
rules: [
{
test: /\.html$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'html-loader',
options: {
minimize: !devMode,
},
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'simple-scaffold',
template: path.resolve(__dirname, '../src/index.html'),
filename: 'index.html',
}),
],
}
删除了mode,mode: 'none',,改变了publicPath的值publicPath: '/',(`publicPath: './')。
当process.env.NODE_ENV !== 'production'时表明是开发环境,在开发环境不对html进行压缩minimize: !devMode,。在下文的webpack.prod.js文件中已经配置了mode为production,当设置mode值为production的时候,会自动设置process.env.NODE_ENV 为production。在入口文件中拿到的process.env.NODE_ENV为production,但是要想在配置文件中使用环境变量,必须使用cross-env NODE_ENV=production先设置好环境变量,否则webpack.common.js文件中是拿到的process.env.NODE_ENV为undefined。
在生产环境,假如打包后的文件为app.js,使用CDN的时候,用户请求的路径app.js,这个路径拿到的可能是服务器上缓存的数据,而不是最新的数据,所以打包后的文件需要一个hash值,当文件内容变化的时候这个hash值也会变化,保证用户拿到的是正确的文件。但是开发环境中没有这个需求,所以开发环境的文件名不需要使用hash(filename: devMode ? '[name].js' : '[name].[hash].js',)。
webpack.dev.js:
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
open: true,
hot: true,
},
});
mode设置为development会针对开发环境进行一些处理,比如将process.env.NODE_ENV设置为development。
使用devtool配置为inline-source-map,源码映射会以一个DataUrl的方式添加到打包的文件中。执行npm run build:dev将文件打包后,可以看见打包后的js文件多了以下代码:
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2...
在devServer中进行webpack-dev-server的配置,open表示打开默认浏览器,配置hot为true的时候会使用hot-module-replacement让文件内容改变时,重新加载相应模块(不是重新加载整个应用)。执行npm start就会启动服务,打开默认浏览器,在浏览器中看到“使用Webpack等搭建一个适用于React项目的脚手架”这几个字。
webpack.prod.js:
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
output: {
publicPath: '/',
},
});
当设置mode为production的时候,Webpack会将process.env.NODE_ENV设置为production。并且自动配置了terser-webpack-plugin,这个插件利用terser过滤掉js中多余的内容,包括去掉js中的注释和空格。
设置devtool为source-map会生成单独的source map文件,并且在打包好的js中有一行注释,指明去哪儿找这个文件。执行npm run build 打包代码:
Asset Size Chunks Chunk Names
app.da19f0220d534a1d34d1.js 1.15 KiB 0 [emitted] [immutable] app
app.da19f0220d534a1d34d1.js.map 5.05 KiB 0 [emitted] [dev] app
index.html 333 bytes [emitted]
可以看见有一个.map文件,并且app.da19f0220d534a1d34d1.js 中的有以下注释:
//# sourceMappingURL=app.da19f0220d534a1d34d1.js.map
使用React
1.使用React创建一个简单的页面。
安装react和react-dom:
npm i --save react react-dom
react只包含了定义React组件的必要功能,它一般和React渲染器配合使用,web应用是用react-dom渲染器,native应用使用react-native渲染器。
src包含文件:
...
└── src
├── components
│ └── Header
│ └── Header.js
├── index.html
├── index.js
└── pages
└── Home
└── Home.js
index.js文件内容:
import ReactDOM from 'react-dom';
import Home from './pages/Home/Home';
ReactDOM.render(
<Home />,
document.getElementById('root')
);
Home.js文件内容:
import Header from '@/components/Header/Header';
export default function Home() {
return (
<div>
<Header userName="任沫" />
<h1>使用Webpack等搭建一个适用于React项目的脚手架</h1>
</div>
);
}
Header.js文件的内容:
export default function Header (props) {
return (
<div>
<p>{props.userName}</p>
</div>
);
}
2.使用@babel/preset-react转化JSX语法。
安装babel-loader、@babel/core、@babel/preset-react:
npm i --save-dev babel-loader @babel/core @babel/preset-react
babel-loader使用babel解析文件,@babel/core是babel的核心模块,
在根目录下创建.babelrc.js文件用于进行babel配置。
touch .babelrc.js
.babelrc.js文件内容:
module.exports = {
presets: ['@babel/preset-react'],
};
presets相当于一系列插件的合集,使用presets就不用一个个设置插件了。比如@babel/preset-react一般情况下会包含@babel/plugin-syntax-jsx、@babel/plugin-transform-react-jsx、@babel/plugin-transform-react-display-name这几个babel插件。
3.Webpack配置(webpack.common.js):
const webpack = require('webpack');
...
module.exports = {
...
resolve: {
extensions: ['.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, '../src'),
},
},
module: {
rules: [
...
{
test: /\.js(x?)$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'babel-loader?cacheDirectory=true',
},
],
},
plugins: [
...
new webpack.ProvidePlugin({
React: 'react',
}),
],
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
},
},
},
},
}
在extensions中定义好文件后缀名后,在import文件的时候,就可以不加文件后缀名了。webpack会按照定义的后缀名的顺序依次处理文件,比如上文配置 extensions: ['.js', '.jsx'],引入模块import Home from './pages/Home/Home';的时候,webpack会先尝试加上.js后缀,看找得到文件不,如果找不到就尝试加上.jsx后缀名继续查找。
alias中定义了src文件目录的别名是@,这样在文件中引入别的文件的时候,可以直接使用@,而不是去找文件的相对路径。
使用webpack.ProvidePlugin定义自动查找的标志符,上面代码中的React: 'react',指的是当需要变量React的时候,会自动到当前目录或者node_modules中去找react模块。这样就不用在每个组件文件中都使用一次import React from 'react'了。
使用cacheDirectory缓存loader的执行结果。loader: 'babel-loader?cacheDirectory=true',这样设置会使用默认缓存目录node_modules/.cache/babel-loader。
splitChunks将通用的模块打包为单独的一个文件,如果不配置splitChunks,那么代码会全部打包到app.hash.js中,导致app.hash.js文件很大,js越大,请求js文件和执行文件的时间越长,页面呈现给用户的耗时就越久。上面代码中的配置只将node_modules中用到的模块打包成一个文件(不会打包node_modules外的模块)。
执行npm start查看页面。
使用TypeScript
首先安装好所需依赖:
npm i --save-dev typescript @babel/preset-typescript
npm i --save-dev @types/react @types/react-dom
@types/react中包含react的类型定义,@types/react-dom中包含react-dom的类型定义。
@babel/preset-typescript是一个babel的preset,用于处理TypeScript。
1.修改**.babelrc.js**:
module.exports = {
presets: [
'@babel/preset-react',
'@babel/preset-typescript',
],
};
preset的执行顺序是从后到前的。根据以上代码的babel配置,会先执行@babel/preset-typescript,然后再执行@babel/preset-react。
2.在根目录创建tsconfig.json文件,用于TypeScript配置:
{
"compilerOptions": {
"baseUrl": "./",
"jsx": "react",
"paths": {
"@/*": ["src/*"]
},
"esModuleInterop": true,
},
"include": [
"src/*",
"typings/*"
]
}
baseUrl设置基础路径。jsx用来设置jsx语法是以什么方式转换为JavaScript的。esModuleInterop为true允许使用import React from 'react',否则对于没有默认导出的模块,比如react,必须使用import * as React from 'react'。include设置typescript处理的文件范围。
之前的代码中,通过webpack.ProvidePlugin定义了代表react包的标志符React,TypeScript中需要为React定义类型,按照这个issue中geekflyer的回答进行了全局的类型定义:
mkdir typings && cd typings && touch react.d.ts
react.d.ts文件的内容为:
import React from 'react';
declare global {
const React: typeof React;
}
上文的Webpack中定义了路径的别名@,在tsconfig.json中也需要做相应的配置:
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
在paths中定义路径的别名。必须设置了baseUrl选项,才能使用paths选项。
更多的Typescript配置在tsconfig文档能找到。
3.Webpack配置
使用TypeScript后,webpack.common.js中的内容也许做以下调整:
...
module.exports = {
entry: {
app: path.resolve(__dirname, '../src/index.tsx'),
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
...
},
module: {
rules: [
...
{
test: /(\.js(x?))|(\.ts(x?))$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'babel-loader?cacheDirectory=true',
},
],
},
...
4.使用TypeScript
将index.js、Home.js、Header.js的后缀改为.tsx。
修改Header.tsx
interface HeaderProps {
userName: string;
}
export default function Header (props: HeaderProps): JSX.Element {
return (
<div>
<p>{props.userName}</p>
</div>
);
}
将Home.tsx中的<Header userName="任沫" />改为<Header />就会出现一个类型错误的提示:
Property 'userName' is missing in type '{}' but required in type 'HeaderProps'.ts(2741)
执行npm start查看页面。
使用babel
babel的作用是将代码转换为在浏览器上能正常运行的代码。其实上文中已经使用babel来解析JSX和TypeScript了。但是解析过JSX和TypeScript之后得到的JavaScript可能依然无法在某些浏览器上正常运行,所以需要使用@babel/preset-env,@babel/preset-env根据设置的目标环境找出所需的插件,并将插件列表传给babel,这样只需配置好目标环境,其他的babel会进行处理。
安装依赖:npm i --save-dev @babel/preset-env
修改**.babelrc.js**:
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: '> 2% in CN and not ie <= 8 and not dead',
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
};
targets: '> 2% in CN and not ie <= 8 and not dead',这里配置的targets的意思是,选择目标环境为:中国区统计数据为2%以上的浏览器,不包括版本号小于8的IE浏览,不包括官方已经不维护的浏览器。
执行npm start,在谷歌浏览器中能看到预期的页面。
下一篇:《使用Webpack等搭建一个适用于React项目的脚手架(2 - React Router、Redux、Sass)》。