react-sample-javascript
为了实现一个可定制化高的react工程,我们往往会自己搭建一个react工程。所以本文会从零开始搭建一个react脚手架工程。解释webpack中配置的含义。
基于webpack 4.0 。包含 开发环境配置,生产环境配置,代码分离,css提取,gzip压缩,base64加载资源,打包分析,ssh一键部署等常用配置。
github项目地址
[TOC]
项目初始化
统一规范代码格式
- 配置
.editorconfig
使得IDE的方式统一 (见代码) - 配置
.eslintrc.js
使得代码规范统一 (见代码)
预期功能
- 管理资源: 能加载css、sccc、less、以及静态文件
- 管理输出:将打包后的静态文件输出至static目录下,以各自的文件类型管理
- dev:使用source map,方便调试时代码定位
- dev:配置devServer,并配置热替换,热加载,自动刷新,自动打开浏览器,并预留proxyTable
- dev:设置默认打开8080,被占用则寻找下一个空接口
- production:代码分离,打包css文件,css代码压缩,js代码压缩,输出到模板html,配置gzip
- analysis::使用BundleAnalyzerPlugin 分析打包后的性能
目录结构
:.
│ .babelrc #babel的规则以及插件
│ .editorconfig #IDE/编辑器相关的配置
│ .eslintignore #Eslint忽视的目录
│ .eslintrc.js #Eslint的规则和插件
│ .gitignore #Git忽视的目录
│ .postcssrc.js #postcss的插件
│ package-lock.json
│ package.json #项目相关的包
│ README.md
│ yarn.lock
│
├─build #webpack相关的配置
│ utils.js #webpack配置中的通用方法
│ webpack.base.conf.js #webpack的基础配置
│ webpack.dev.conf.js #webpack的开发环境配置
│ webpack.prod.conf.js #webpack的生产环境配置
│
└─src #主目录,业务代码
│ app.css
│ App.js
│ favicon.ico
│ index.ejs
│ index.js
│
└─assets #静态目录,存放静态资源
│ config.json
│
└─img
logo.svg
安装依赖
- eslint-loader
- eslint
- eslint-config-airbnb
- eslint-plugin-import
- eslint-friendly-formatter
- eslint-plugin-flowtype
- eslint-plugin-jsx-a11y
- eslint-plugin-react
- babel-polyfill
- webpack
- jest
- friendly-errors-webpack-plugin
编译提示的webpack插件
- html-webpack-plugin
新建html入口文件的webpack插件
- copy-webpack-plugin
webpack配置合并模块
- webpack-merge
webpack配置合并模块
- webpack-dev-server
- webpack-bundle-analyzer
- webpack-cli
- portfinder 寻找接口的插件
- extract-text-webpack-plugin
- node-notifier
- optimize-css-assets-webpack-plugin
- autoprefixer
- mini-css-extract-plugin
- autoprefixer
- css-loader
- less-loader
- postcss-loader
- postcss-import
- postcss-loader
- style-loader
- babel-core
- babel-eslint
- babel-loader
- babel-plugin-transform-runtime
- babel-plugin-import
- babel-preset-env
- babel-preset-react
- babel-polyfill
- url-loader
- cross-env
- file-loader
yarn add eslint eslint-loader eslint-config-airbnb eslint-plugin-import eslint-friendly-formatter eslint-plugin-flowtype eslint-plugin-jsx-a11y eslint-plugin-react babel-polyfill webpack jest webpack-merge copy-webpack-plugin html-webpack-plugin friendly-errors-webpack-plugin webpack-dev-server webpack-bundle-analyzer webpack-cli portfinder extract-text-webpack-plugin node-notifier optimize-css-assets-webpack-plugin autoprefixer mini-css-extract-plugin autoprefixer css-loader less-loader postcss-loader postcss-import postcss-loader style-loader babel-core babel-eslint babel-loader babel-plugin-transform-runtime babel-plugin-import babel-preset-env babel-preset-react babel-polyfill url-loader cross-env file-loader -D
项目配置
webpack 基础配置
- 为了控制开发环境和生产环境,我们可以新建build文件夹。分别书写开发环境和生产环境的webpack配置文件,这样也更可以方便我们分别控制生产环境和开发环境。
- 为了提高代码的复用率,也为了区别
基础配置
和个性配置
,可以分别新建webpack.base
、webpack.dev
和webpack.prod
三个配置文件。首先配置最基础的entry(入口)和output(出口)。
module.exports = {
context: path.resolve(__dirname, '../'), //绝对路径。__dirname为当前目录。
//基础目录用于从配置中解析入口起点。因为webpack配置在build下,所以传入 '../'
entry: {
app: ('./src/index.js') //项目的入口
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].[hash:8].js',
publicPath: '/',
libraryTarget: 'umd',
},
}
entry
entry可以分别为字符串、数组和对象。
倘若应用只有一个单一的入口,entry的值可以使用任意类型,不会影响输出结果。
// entry为字符串
{
entry: './src/index.js',
output: {
path: '/dist',
filename: 'bundle.js'
}
}
// 结果会生成 '/dist/bundle.js'
// entry为数组,可以添加多个彼此不互相依赖的文件。结合output.library选项,如果传入数组,则只导出最后一项。
{
//如果你在html文件里引入了'bable-polyfill',可以通过数组将它加到bundle.js的最后。
entry: ['./src/index.js', 'babel-polyfill'] ,
output:{
path: '/dist',
filename: 'bundle.js'
}
}
// entry为对象,可以将页面配置为多页面的而不是SPA,有多个html文件。通过对象告诉webpack为每个入口,成一个bundle文件。
// 多页面的配置,可能还要借助于HtmlWebpackPlugin,来指定每个html需要引入的js
{
entry: {
index: './src/index.js'
main: './src/index.js'
login: './src/login.js'
}
output:{
path: '/dist/pages'
filename: '[name]-[hash:5].js' //文件名取自'entry'对象的键名,为了防止推送代码后浏览器读缓存,故再生成的文件之后加上hash码。
}
}
// 会分别生成index.js,main.js,login.js三个文件
关于 webpack构建多页面 可以参考这篇文章。不过现在webpack4.x也是一次断崖式升级,感兴趣的同学可以自行搜索。
// entry也可以传入混合类型
{
entry:{
vendor: ['jquery','amap','babel-polyfill'] //也可以借助CommonsChunkPlugin提取vendor的chunk。
index: './src/index.js'
}
output: {
path: '/dist'
filename: '[name]-[hash:5].js'
}
}
CommonsChunkPlugin在webpack4.0之后移除了,可以使用splitChunksPlugin代替。
可以参阅如下链接:optimization.splitChunks
output
output最基础的两个配置为 path
和 filename
:
-
path
告诉 webpack的输出目录在那里,一般我们会设置在根目录的dist
文件夹; -
filename
用于指定输出文件的文件名,如果配置了创建了多个单独的chunk
则可以使用[name].[hash]
这种占位符来确保每个文件有唯一的名称; - 另一个常见配置
publicPath
则是用于更加复杂的场景。举例:在本地时,你可能会使用../assets/test.png
这种url来载入图片。而在生产环境下,你可能会使用CDN或者图床的地址。那么就需要配置publicPath = "http://cdn.example.com/assets/"
来实现生产模式下编译输出文件时自动更新url。
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].[hash:8].js',
publicPath: '/',
},
resolve
resolve常用的两个配置为 alias
和 extensions
:
-
alias
创建import或者require的别名 -
extensins
自动解析文件拓展名,补全文件后缀
resolve: {
// 自动解析文件扩展名(补全文件后缀)(从左->右)
// import hello from './hello' (!hello.js? -> !hello.jsx? -> !hello.json)
extensions: ['.js', '.jsx', '.json'],
alias: {
'@': resolve('src')
}
},
module
module的选项决定了如何处理项目中的不同类型的模块。其中常用的有 rules
和 noParese
两个配置项。
-
noParese
是为了防止weback解析与所有与rule相匹配的文件。目的是,忽略大型的library可以提高构建性能。
noParse: function(content) {
return /jquery|lodash/.test(content);
}
-
rules
用于在创建模块是,匹配规则数组,以确定哪些规则能够对module应用loader,或者是修改parser。
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
enforce: 'pre',
use: [{
loader: 'babel-loader',
}, {
loader: 'eslint-loader', // 指定启用eslint-loader
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: false
}
}]
},
{
test: /\.css$/,
include: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [autoprefixer({ browsers: 'last 5 versions' })],
sourceMap: false,
},
},
],
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: ('assets/img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: ('assets/media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: ('assets/fonts/[name].[hash:7].[ext]')
}
}
]
}
例如上述代码,就使用eslint-lodaer
和 babel-loader
处理了除了node_modules
以外的 js||jsx
。同时配置了,解析图片、视频、字体文件等的解析,当rules匹配到的文件时,小于10000 byte 时,采用url-loader解析文件。
Webpack开发配置
因为在webpack 4.X 中使用了流行的 ”约定大于配置“ 的做法,所以在新加入配置项 mode
,可以告知webpack使用相应模式的内置优化。
选项 | 描述 |
---|---|
development | 会将process.env.NODE_ENV 的值设为 development 。启用 NamedChunksPlugin 和 NamedMoudulesPlugin 。 |
production | 会将process.env.NODE_ENV 的值设为 production 。启用 FlagDependencyUsagePlugin ,FlagIncludedChunksPlugin ,ModuleConcatenationPlugin ,NoEmitOnErrorsPlugin ,OccurrenceOrderPlugin ,SideEffectsFlagPlugin 和UglifyJsPlugin 。 |
如果我们只设置NODE_ENV,则不会自动设置
mode
在开发时,我们往往希望能看到当前开发的页面,并且能热加载。这时,我们可以借助webpack-dev-server 这个插件,来在项目中起一个应用服务器。
// package.json
"scripts": {
"start": "webpack-dev-server --mode development --config build/webpack.dev.conf.js",
}
// 设置当前的mode为development,同样这个配置也可以写在webpack.dev.conf.js中。然后使用build目录下的webpack.dev.conf.js 来配置相关的webpack。
devServer: {
clientLogLevel: 'warning',
historyApiFallback: true, //在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html
contentBase: path.resolve(__dirname, '../src'),
compress: true,
hot: true, // 热加载
inline: true, //自动刷新
open: true, //自动打开浏览器
host: HOST||'localhost',
port: PORT,
overlay: { warnings: false, errors: true }, // 在浏览器上全屏显示编译的errors或warnings。
publicPath: '/',
proxy: {},
quiet: true, // necessary for FriendlyErrorsPlugin // 终端输出的只有初始启动信息。 webpack 的警告和错误是不输出到终端的
watchOptions: {
poll: false
}
},
plugins: [
new webpack.DefinePlugin({
...process.env
}),
//开启HMR(热替换功能,替换更新部分,不重载页面!)
new webpack.HotModuleReplacementPlugin(),// HMR shows correct file names in console on update.
//显示模块相对路径
new webpack.NamedModulesPlugin(),
//不显示错误信息
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
]
其实在开发时,我们可以设置 contentBase: '/src'
,contentBase
指定了devServer能访问的资源地址。因为我们开发时,资源大部分都放在src
目录下,所以可以直接指定资源路径为src
目录。因为我们在webpack基础配置时,配置了 output
输出为 dist
目录,所以我们也可以在devServer里,设置 contentBase
为 dist
目录。不过此时需要使用copyWebpackPlugin将一些静态资源复制到 dist
目录下,手动新建dist目录,并复制也可以。
另外,当使用 history 路由时,要配置 historyApiFallback = true
,以此让服务器放弃路由权限,交由前端路由。而是用 hash 路由则不需要此配置。
项目进阶
生产环境配置
在使用webpack 4.x 的 mode 配置之后,需要我们手动配置的项已经减少了很多,像js代码压缩这种工具 UglifyJsPlugin
就已经不用手动去配置。但是像很多前面提到的 代码分离
、css代码提取和压缩
、html的生成
以及 复制静态资源
还需要我们手动配置。
代码分离
// 设置代码分离的输出目录
output: {
path: path.resolve(__dirname, '../dist'),
filename: ('js/[name].[hash:8].js'),
chunkFilename: ('js/[name]-[id].[hash:8].js')
},
// 代码分离
optimization: {
runtimeChunk: {
name: "manifest"
},
splitChunks: {
chunks: 'all'
}
},
可以参阅如下链接:
optimization.splitChunks
css代码压缩
借助 MiniCssExtractPlugin
来实现压缩css和提取css。因为 MiniCssExtractPlugin
无法与style-loader 共存,所以我们需要判断当前环境是生成环境还是开发环境。
我们可以新建一个util.js的文件,在webpack当中一些共用的方法。考虑使用个别配置字段 extract
来配置使用何种方式来配置css-loader。参见 util.js
代码。
new MiniCssExtractPlugin({
filename: 'css/[name].[hash:8].css',
chunkFilename: 'css/[name]-[id].[hash:8].css',
}),
生成HTML
使用htmlWebpackPlugin
,配合ejs。可以使控制html 的生成。通过配置的方式,生成html。因为 HtmlWebpackPlugin
本身可以解析ejs,所以不需要单独引入ejs的loader。
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.ejs', // 设置目录
title: 'React Demo',
inject: true, // true->'head' || false->'body'
minify: {
//删除Html注释
removeComments: true,
//去除空格
collapseWhitespace: true,
//去除属性引号
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<% for (var chunk in htmlWebpackPlugin.files.css) { %>
<link rel="preload" href="<%= htmlWebpackPlugin.files.css[chunk] %>" as="style">
<% } %>
<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
<link rel="preload" href="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>" as="script">
<% } %>
<base href="/">
</head>
<body>
<div id="root"></div>
</body>
<style type="text/css">
body {
font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;
}
</style>
</html>
复制静态目录
将所以可能被请求的静态文件,分别放在assets目录下。那么在打包后,为了保证目录能正常访问(不使用CDN等加载静态资源时),我们可以配置 publicPath = '/'
。然后借助于 CopyWebpackPlugin
实现资源复制。
new CopyWebpackPlugin([{
from: './src/assets/',
to: 'assets'
}]),
将 src/assets
复制到 dist/assets
目录下。
开启打包分析
借助插件 BundleAnalyzerPlugin
直接在plugins中创建该插件:
// webpack.prod.conf.js
const BundleAnalyzerPlugin = process.env.NODE_ENV=== 'analysis' ? require('webpack-bundle-analyzer').BundleAnalyzerPlugin:null
process.env.NODE_ENV=== 'analysis' ? new BundleAnalyzerPlugin() : ()=>{}
在package.json 中可做如下配置:
"scripts": {
"analysis": "cross-env NODE_ENV=analysis webpack -p --mode production --progress --config ./build/webpack.prod.conf.js ",
},
通过注入环境变量,来控制是否运行打包分析。
ssh部署
打包后的dist文件夹,可以直接借助 node 的 ssh-node ,直接部署到服务器指定的目录下。 ssh-node既支持ssh,也支持密码登录。建议可以为在每个项目下,新建一个.ssh文件,存放项目的私钥。代码如下:
// usage: https://www.npmjs.com/package/node-ssh
var path, node_ssh, ssh, fs, opn, host
fs = require('fs')
path = require('path')
node_ssh = require('node-ssh')
opn = new require('opn')
ssh = new node_ssh()
host = 'localhost'
var localDir = './dist'
var remoteDir = '/opt/frontend/new'
var removeCommand = 'rm -rf ./*'
var pwdCommand = 'pwd'
ssh.connect({
host: host,
username: 'root',
port: 22,
// password,
privateKey: "./.ssh/id_rsa",
})
.then(function() {
ssh.execCommand(removeCommand, { cwd:remoteDir }).then(function(result) {
console.log('STDOUT: ' + result.stdout)
console.log('STDERR: ' + result.stderr)
ssh.putDirectory(localDir, remoteDir).then(function() {
console.log("The File thing is done")
ssh.dispose()
opn('http://'+host, {app:['chrome']})
}, function(error) {
console.log("Something's wrong")
console.log(error)
ssh.dispose()
})
})
})
此时,在命令行直接 node deploy.js
就可以运行以上脚本,我们也可以添加一个build + deploy的script脚本,便于启动。
"scripts": {
"depoly": "npm run build && node ./deploy.js",
}
结语
本次从零到一,新建了一个react脚手架。过程中有很多问题,也参考了不少大牛的解释。代码里也有诸多问题。还望各位看官,不吝指教。