使用 webpack + react + redux + es6 开发组件化前端项目
因为最近在工作中尝试了 webpack、react、redux、es6 技术栈,所以总结出了一套 boilerplate,以便下次做项目时可以快速开始,并进行持续优化。对应的项目地址:webpack-react-redux-es6-boilerplate
该项目的 webpack 配置做了不少优化,所以构建速度还不错。文章的最后还对使用 webpack 的问题及性能优化作出了总结。
项目结构规划
每个模块相关的 css、img、js 文件都放在一起,比较直观,删除模块时也会方便许多。测试文件也同样放在一起,哪些模块有没有写测试,哪些测试应该一起随模块删除,一目了然。
build
|-- webpack.config.js # 公共配置
|-- webpack.dev.js # 开发配置
|-- webpack.release.js # 发布配置
docs # 项目文档
node_modules
src # 项目源码
|-- conf # 配置文件
|-- pages # 页面目录
| |-- page1
| | |-- index.js # 页面逻辑
| | |-- index.scss # 页面样式
| | |-- img # 页面图片
| | | |-- xx.png
| | |-- __tests__ # 测试文件
| | | |-- xx.js
| |-- app.html # 入口页
| |-- app.js # 入口JS
|-- components # 组件目录
| |-- loading
| | |-- index.js
| | |-- index.scss
| | |-- __tests__
| | | |-- xx.js
|-- js
| |-- actions
| | |-- index.js
| | |-- __tests__
| | | |-- xx.js
| |-- reducers
| | |-- index.js
| | |-- __tests__
| | | |-- xx.js
| |-- xx.js
|-- css # 公共CSS目录
| |-- common.scss
|-- img # 公共图片目录
| |-- xx.png
tests # 其他测试文件
package.json
READNE.md
要完成的功能
编译 jsx、es6、scss 等资源
自动引入静态资源到相应 html 页面
实时编译和刷新浏览器
按指定模块化规范自动包装模块
自动给 css 添加浏览器内核前缀
按需打包合并 js、css
压缩 js、css、html
图片路径处理、压缩、CssSprite
对文件使用 hash 命名,做强缓存
语法检查
全局替换指定字符串
本地接口模拟服务
发布到远端机
针对以上的几点功能,接下来将一步一步的来完成这个 boilerplate 项目, 并记录下每一步的要点。
准备工作
1、根据前面的项目结构规划创建项目骨架
$ make dir webpack-react-redux-es6-boilerplate
$ cd webpack-react-redux-es6-boilerplate
$ mkdir build docs src mock tests
$ touch build/webpack.config.js build/webpack.dev.js build/webpack.release.js
// 创建 package.json
$ npm init
$ ...
2、安装最基本的几个 npm 包
$ npm i webpack webpack-dev-server --save-dev
$ npm i react react-dom react-router redux react-redux redux-thunk --save
3、编写示例代码,最终代码直接查看 boilerplate
4、根据 webpack 文档编写最基本的 webpack 配置,直接使用 NODE API 的方式
/* webpack.config.js */
var webpack = require('webpack');
// 辅助函数
var utils = require('./utils');
var fullPath = utils.fullPath;
var pickFiles = utils.pickFiles;
// 项目根路径
var ROOT_PATH = fullPath('../');
// 项目源码路径
var SRC_PATH = ROOT_PATH + '/src';
// 产出路径
var DIST_PATH = ROOT_PATH + '/dist';
// 是否是开发环境
var __DEV__ = process.env.NODE_ENV !== 'production';
// conf
var alias = pickFiles({
id: /(conf\/[^\/]+).js$/,
pattern: SRC_PATH + '/conf/*.js'
});
// components
alias = Object.assign(alias, pickFiles({
id: /(components\/[^\/]+)/,
pattern: SRC_PATH + '/components/*/index.js'
}));
// reducers
alias = Object.assign(alias, pickFiles({
id: /(reducers\/[^\/]+).js/,
pattern: SRC_PATH + '/js/reducers/*'
}));
// actions
alias = Object.assign(alias, pickFiles({
id: /(actions\/[^\/]+).js/,
pattern: SRC_PATH + '/js/actions/*'
}));
var config = {
context: SRC_PATH,
entry: {
app: ['./pages/app.js']
},
output: {
path: DIST_PATH,
filename: 'js/bundle.js'
},
module: {},
resolve: {
alias: alias
},
plugins: [
new webpack.DefinePlugin({
// http://stackoverflow.com/questions/30030031/passing-environment-dependent-variables-in-webpack
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || 'development')
})
]
};
module.exports = config;
/* webpack.dev.js */
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.config');
var utils = require('./utils');
var PORT = 8080;
var HOST = utils.getIP();
var args = process.argv;
var hot = args.indexOf('--hot') > -1;
var deploy = args.indexOf('--deploy') > -1;
// 本地环境静态资源路径
var localPublicPath = 'http://' + HOST + ':' + PORT + '/';
config.output.publicPath = localPublicPath;
config.entry.app.unshift('webpack-dev-server/client?' + localPublicPath);
new WebpackDevServer(webpack(config), {
hot: hot,
inline: true,
compress: true,
stats: {
chunks: false,
children: false,
colors: true
},
// Set this as true if you want to access dev server from arbitrary url.
// This is handy if you are using a html5 router.
historyApiFallback: true,
}).listen(PORT, HOST, function() {
console.log(localPublicPath);
});
上面的配置写好后就可以开始构建了
$ node build/webpack.dev.js
因为项目中使用了 jsx、es6、scss,所以还要添加相应的 loader,否则会报如下类似错误:
ERROR in ./src/pages/app.js
Module parse failed: /Users/xiaoyan/working/webpack-react-redux-es6-boilerplate/src/pages/app.js Unexpected token (18:6)
You may need an appropriate loader to handle this file type.
编译 jsx、es6、scss 等资源
使用 bael 和 babel-loader 编译 jsx、es6
安装插件: babel-preset-es2015 用于解析
es6安装插件:babel-preset-react 用于解析
jsx
// 首先需要安装 babel
$ npm i babel-core --save-dev
// 安装插件
$ npm i babel-preset-es2015 babel-preset-react --save-dev
// 安装 loader
$ npm i babel-loader --save-dev
在项目根目录创建 .babelrc 文件:
{
"presets": ["es2015", "react"]
}
在 webpack.config.js 里添加:
// 使用缓存
var CACHE_PATH = ROOT_PATH + '/cache';
// loaders
config.module.loaders = [];
// 使用 babel 编译 jsx、es6
config.module.loaders.push({
test: /\.js$/,
exclude: /node_modules/,
include: SRC_PATH,
// 这里使用 loaders ,因为后面还需要添加 loader
loaders: ['babel?cacheDirectory=' + CACHE_PATH]
});
接下来使用 sass-loader 编译 sass:
$ npm i sass-loader node-sass css-loader style-loader --save-dev
css-loader 用于将 css 当做模块一样来
importstyle-loader 用于自动将 css 添加到页面
在 webpack.config.js 里添加:
// 编译 sass
config.module.loaders.push({
test: /\.(scss|css)$/,
loaders: ['style', 'css', 'sass']
});
自动引入静态资源到相应 html 页面
$ npm i html-webpack-plugin --save-dev
在 webpack.config.js 里添加:
// html 页面
var HtmlwebpackPlugin = require('html-webpack-plugin');
config.plugins.push(
new HtmlwebpackPlugin({
filename: 'index.html',
chunks: ['app'],
template: SRC_PATH + '/pages/app.html'
})
);
至此,整个项目就可以正常跑起来了
$ node build/webpack.dev.js
实时编译和刷新浏览器
完成前面的配置后,项目就已经可以实时编译和自动刷新浏览器了。接下来就配置下热更新,使用 react-hot-loader:
$ npm i react-hot-loader --save-dev
因为热更新只需要在开发时使用,所以在 webpack.dev.config 里添加如下代码:
// 开启热替换相关设置
if (hot === true) {
config.entry.app.unshift('webpack/hot/only-dev-server');
// 注意这里 loaders[0] 是处理 .js 文件的 loader
config.module.loaders[0].loaders.unshift('react-hot');
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
执行下面的命令,并尝试更改 js、css:
$ node build/webpack.dev.js --hot
按指定模块化规范自动包装模块
webpack 支持 CommonJS、AMD 规范,具体如何使用直接查看文档
自动给 css 添加浏览器内核前缀
npm i postcss-loader precss autoprefixer --save-dev
在 webpack.config.js 里添加:
// 编译 sass
config.module.loaders.push({
test: /\.(scss|css)$/,
loaders: ['style', 'css', 'sass', 'postcss']
});
// css autoprefix
var precss = require('precss');
var autoprefixer = require('autoprefixer');
config.postcss = function() {
return [precss, autoprefixer];
}
打包合并 js、css
webpack 默认将所有模块都打包成一个 bundle,并提供了 Code Splitting 功能便于我们按需拆分。在这个例子里我们把框架和库都拆分出来:
在 webpack.config.js 添加:
config.entry.lib = [
'react', 'react-dom', 'react-router',
'redux', 'react-redux', 'redux-thunk'
]
config.output.filename = 'js/[name].js';
config.plugins.push(
new webpack.optimize.CommonsChunkPlugin('lib', 'js/lib.js')
);
// 别忘了将 lib 添加到 html 页面
// chunks: ['app', 'lib']
如何拆分 CSS:separate css bundle
压缩 js、css、html、png 图片
压缩资源最好只在生产环境时使用
// 压缩 js、css
config.plugins.push(
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
);
// 压缩 html
// html 页面
var HtmlwebpackPlugin = require('html-webpack-plugin');
config.plugins.push(
new HtmlwebpackPlugin({
filename: 'index.html',
chunks: ['app', 'lib'],
template: SRC_PATH + '/pages/app.html',
minify: {
collapseWhitespace: true,
collapseInlineTagWhitespace: true,
removeRedundantAttributes: true,
removeEmptyAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
removeComments: true
}
})
);
图片路径处理、压缩、CssSprite
压缩图片使用 image-webpack-loader
图片路径处理使用 url-loader
$ npm i url-loader image-webpack-loader --save-dev
在 webpack.config.js 里添加:
// 图片路径处理,压缩
config.module.loaders.push({
test: /\.(?:jpg|gif|png|svg)$/,
loaders: [
'url?limit=8000&name=img/[hash].[ext]',
'image-webpack'
]
});
雪碧图处理:webpack_auto_sprites
对文件使用 hash 命名,做强缓存
根据 docs,在产出文件命名中加上 [hash]
config.output.filename = 'js/[name].[hash].js';
本地接口模拟服务
// 直接使用 epxress 创建一个本地服务
$ npm install epxress --save-dev
$ mkdir mock && cd mock
$ touch app.js
var express = require('express');
var app = express();
// 设置跨域访问,方便开发
app.all('*', function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
next();
});
// 具体接口设置
app.get('/api/test', function(req, res) {
res.send({ code: 200, data: 'your data' });
});
var server = app.listen(3000, function() {
var host = server.address().address;
var port = server.address().port;
console.log('Mock server listening at http://%s:%s', host, port);
});
// 启动服务,如果用 PM2 管理会更方便,增加接口不用自己手动重启服务
$ node app.js &
发布到远端机
写一个 deploy 插件,使用 ftp 上传文件
$ npm i ftp --save-dev
$ touch build/deploy.plugin.js
// build/deploy.plugin.js
var Client = require('ftp');
var client = new Client();
// 待上传的文件
var __assets__ = [];
// 是否已连接
var __connected__ = false;
var __conf__ = null;
function uploadFile(startTime) {
var file = __assets__.shift();
// 没有文件就关闭连接
if (!file) return client.end();
// 开始上传
client.put(file.source, file.remotePath, function(err) {
// 本次上传耗时
var timming = Date.now() - startTime;
if (err) {
console.log('error ', err);
console.log('upload fail -', file.remotePath);
} else {
console.log('upload success -', file.remotePath, timming + 'ms');
}
// 每次上传之后检测下是否还有文件需要上传,如果没有就关闭连接
if (__assets__.length === 0) {
client.end();
} else {
uploadFile();
}
});
}
// 发起连接
function connect(conf) {
if (!__connected__) {
client.connect(__conf__);
}
}
// 连接成功
client.on('ready', function() {
__connected__ = true;
uploadFile(Date.now());
});
// 连接已关闭
client.on('close', function() {
__connected__ = false;
// 连接关闭后,如果发现还有文件需要上传就重新发起连接
if (__assets__.length > 0) connect();
});
/**
* [deploy description]
* @param {Array} assets 待 deploy 的文件
* file.source buffer
* file.remotePath path
*/
function deployWithFtp(conf, assets, callback) {
__conf__ = conf;
__assets__ = __assets__.concat(assets);
connect();
}
var path = require('path');
/**
* [DeployPlugin description]
* @param {Array} options
* option.reg
* option.to
*/
function DeployPlugin(conf, options) {
this.conf = conf;
this.options = options;
}
DeployPlugin.prototype.apply = function(compiler) {
var conf = this.conf;
var options = this.options;
compiler.plugin('done', function(stats) {
var files = [];
var assets = stats.compilation.assets;
for (var name in assets) {
options.map(function(cfg) {
if (cfg.reg.test(name)) {
files.push({
localPath: name,
remotePath: path.join(cfg.to, name),
source: new Buffer(assets[name].source(), 'utf-8')
});
}
});
}
deployWithFtp(conf, files);
});
};
module.exports = DeployPlugin;
运用上面写的插件,实现同时在本地、测试环境开发,并能自动刷新和热更新。在 webpack.dev.js 里添加:
var DeployPlugin = require('./deploy.plugin');
// 是否发布到测试环境
if (deploy === true) {
config.plugins.push(
new DeployPlugin({
user: 'username',
password: 'password',
host: 'your host',
keepalive: 10000000
},
[{reg: /html$/, to: '/xxx/xxx/xxx/app/views/'}])
);
}
在这个例子里,只将 html 文件发布到测试环境,静态资源还是使用的本地的webpack-dev-server,所以热更新、自动刷新还是可以正常使用
其他的发布插件:
webpack 问题及优化
改变代码时所有的 chunkhash 都会改变
在这个项目中我们把框架和库都打包到了一个 chunk,这部分我们自己是不会修改的,但是当我们更改业务代码时这个 chunk 的 hash 却同时发生了变化。这将导致上线时用户又得重新下载这个根本没有变化的文件。
所以我们不能使用 webpack 提供的 chunkhash 来命名文件,那我们自己根据文件内容来计算 hash 命名不就好了吗。
开发的时候不需要使用 hash,或者使用 hash 也没问题,最终产出时我们使用自己的方式重新命名:
$ npm i md5 --save-dev
$ touch build/rename.plugin.js
// rename.plugin.js
var fs = require('fs');
var path = require('path');
var md5 = require('md5');
function RenamePlugin() {
}
RenamePlugin.prototype.apply = function(compiler) {
compiler.plugin('done', function(stats) {
var htmlFiles = [];
var hashFiles = [];
var assets = stats.compilation.assets;
Object.keys(assets).forEach(function(fileName) {
var file = assets[fileName];
if (/\.(css|js)$/.test(fileName)) {
var hash = md5(file.source());
var newName = fileName.replace(/(.js|.css)$/, '.' + hash + '$1');
hashFiles.push({
originName: fileName,
hashName: newName
});
fs.rename(file.existsAt, file.existsAt.replace(fileName, newName));
}
else if (/\.html$/) {
htmlFiles.push(fileName);
}
});
htmlFiles.forEach(function(fileName) {
var file = assets[fileName];
var contents = file.source();
hashFiles.forEach(function(item) {
contents = contents.replace(item.originName, item.hashName);
});
fs.writeFile(file.existsAt, contents, 'utf-8');
});
});
};
module.exports = RenamePlugin;
在 webpack.release.js 里添加:
// webpack.release.js
var RenamePlugin = require('./rename.plugin');
config.plugins.push(new RenamePlugin());
最后也推荐使用自己的方式,根据最终文件内容计算 hash,因为这样无论谁发布代码,或者无论在哪台机器上发布,计算出来的 hash 都是一样的。不会因为下次上线换了台机器就改变了不需要改变的 hash。
使用 webpack + react + redux + es6 开发组件化前端项目的更多相关文章
- webpack+react+redux+es6开发模式---续
一.前言 之前介绍了webpack+react+redux+es6开发模式 ,这个项目对于一个独立的功能节点来说是没有问题的.假如伴随着源源不断的需求,前段项目会涌现出更多的功能节点,需要独立部署运行 ...
- webpack+react+redux+es6开发模式
一.预备知识 node, npm, react, redux, es6, webpack 二.学习资源 ECMAScript 6入门 React和Redux的连接react-redux Redux 入 ...
- webpack+react+redux+es6
一.预备知识 node, npm, react, redux, es6, webpack 二.学习资源 ECMAScript 6入门 React和Redux的连接react-redux Redux 入 ...
- 前端开发组件化设计vue,react,angular原则漫谈
前端开发组件化设计vue,react,angular原则漫谈 https://www.toutiao.com/a6346443500179505410/?tt_from=weixin&utm_ ...
- [Android Pro] 终极组件化框架项目方案详解
cp from : https://blog.csdn.net/pochenpiji159/article/details/78660844 前言 本文所讲的组件化案例是基于自己开源的组件化框架项目g ...
- Android组件化框架项目详解
简介 什么是组件化? 项目发展到一定阶段时,随着需求的增加以及频繁地变更,项目会越来越大,代码变得越来越臃肿,耦合会越来越多,开发效率也会降低,这个时候我们就需要对旧项目进行重构即模块的拆分,官方的说 ...
- react案例->新闻移动客户端--(react+redux+es6+webpack+es6的spa应用)
今天分享一个react应用,应在第一篇作品中说要做一个react+redux+xxx的应用.已经做完一部分,拿出来分享.github地址为:点我就可以咯~ 这里实现了一个新闻移动站的spa.本来想写p ...
- webpack(8)vue组件化开发的演变过程
前言 真实项目开发过程中,我们都是使用组件化的去开发vue的项目,但是组件化的思想又是如何来的呢?下面就从开始讲解演变过程 演变过程1.0 一般情况下vue都是单页面开发,所以项目中只会有一个inde ...
- React+Redux学习笔记:React+Redux简易开发步骤
前言 React+Redux 分为两部分: UI组件:即React组件,也叫用户自定义UI组件,用于渲染DOM 容器组件:即Redux逻辑,处理数据和业务逻辑,支持所有Redux API,参考之前的文 ...
随机推荐
- MySQL select into
MYSQL不支持: Select * Into new_table_name from old_table_name; Create table new_table_name (Select * fr ...
- 二叉树Binary_Tree(1):二叉树及其数组实现
定义 二叉树: 二叉树是一种特殊的树.二叉树的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树.更加严格的递归定义是:二叉树要么为空,要么由根结点.左子树 ...
- C#Lambda表达式Aggregate的用法及内部运行方式的猜想
, , , , }; // 其和为15 var count = nums.Aggregate((body, next) => { // 注意,nums的元素个数至少一个以上(但如果是有seed的 ...
- 算法分析| 小o和小ω符号
渐近分析的主要思想是对不依赖于机器特定常数的算法的效率进行测量,主要是因为该分析不需要实现算法并且要比较程序所花费的时间. 我们已经讨论了三个主要的渐近符号.本文我们使用以下2个渐近符号表示算法的时间 ...
- 一个APP页面一个接口
目前所在的公司做的是健康产业方面的APP,这个产品包括了安卓和IOS还有web三方面,除了要写后台管理的系统外,还要写移动端的接口.第一次写移动端接口就犯了一个错误,以为和web一样是怎么方便怎么来, ...
- bzoj 4872: [Shoi2017]分手是祝愿
Description Zeit und Raum trennen dich und mich. 时空将你我分开.B 君在玩一个游戏,这个游戏由 n 个灯和 n 个开关组成,给定这 n 个灯的初始状态 ...
- 分享一个Maven的架构,以及在Maven配置中的注意事项
Maven作为一个十分流行的项目管理工具,在我们的项目变得逐渐复杂的时候,可以有效的降低项目管理难度. 聚合工程就是当项目到达一定规模时,十分有效的管理子项目和模块的方法. 首先介绍一下,在大型项目中 ...
- Android短视频SDK转码实践
一. 前言 一些涉及的基本概念: 转码:一般指多媒体文件格式的转换,比如分辨率.码率.封装格式等: 解复用(demux):从某种封装中分离出视频track和音频track,然后交给后续模块进行处理: ...
- qrc资源文件加载后,裸机环境下图片不显示
问题描述:在qt开发环境下,使用qss进行界面美化工作,里面包含许多图片资源.最后项目决定把这些图片资源和代码一起打包.然后就把图片资源和qss文件一起编入qrc文件中进行编译.在本机开发环境下是没有 ...
- Web API系列之二WebApi基础框架搭建
本文主要介绍如何搭建一个WebApi的项目.关于如何搭建WebApi的方式一共有两种: 一.通过vs直接新建一个WebApi的项目,步骤如下: 第一步: 新建一个空的Web应用程序,可以理解为作为We ...