使用 webpack + react + redux + es6 开发组件化前端项目
因为最近在工作中尝试了webpack、react、redux、es6技术栈,所以总结出了一套boilerplate,以便下次做项目时可以快速开始,并进行持续优化。 项目结构规划每个模块相关的 css、img、js 文件都放在一起,比较直观,删除模块时也会方便许多。测试文件也同样放在一起,哪些模块有没有写测试,哪些测试应该一起随模块删除,一目了然。 要完成的功能
针对以上的几点功能,接下来将一步一步的来完成这个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/[^/]+)/,0)">"/components/*/index.js"
}));
// reducers
alias = Object.assign(alias,pickFiles({
id: /(reducers/[^/]+).js/,0)">"/js/reducers/*"
}));
// actions
alias = Object.assign(alias,pickFiles({
id: /(actions/[^/]+).js/,0)">"/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 */
"webpack");
var WebpackDevServer = require("webpack-dev-server");
var config = require("./webpack.config");
"./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 等资源
// 首先需要安装 babel
$ npm i babel-core --save-dev
// 安装插件
$ npm i babel-preset-es2015 babel-preset-react --save-dev
// 安装 loader
$ npm i babel-loader --save-dev
在项目根目录创建 {
"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,150)">// 这里使用 loaders ,因为后面还需要添加 loader
loaders: ["babel?cacheDirectory=" + CACHE_PATH]
});
接下来使用sass-loader编译 sass: $ npm i sass-loader node-sass css-loader style-loader --save-dev
// 编译 sass config.module.loaders.push({ test: /.(scss|css)$/,loaders: ["style",0)">"css",0)">"sass"] }); 自动引入静态资源到相应 html 页面
$ npm i html-webpack-plugin --save-dev // 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 添加浏览器内核前缀使用postcss-loader npm i postcss-loader precss autoprefixer --save-dev "sass",0)">"postcss"] }); // css autoprefix var precss = require("precss"); var autoprefixer = require("autoprefixer"); config.postcss = function() { return [precss,autoprefixer]; } 打包合并 js、csswebpack 默认将所有模块都打包成一个 bundle,并提供了Code Splitting功能便于我们按需拆分。在这个例子里我们把框架和库都拆分出来: 在 webpack.config.js 添加: config.entry.lib = [
"react",0)">"react-dom",0)">"react-router",0)">"redux",0)">"react-redux",0)">"redux-thunk"
]
config.output.filename = "js/[name].js";
config.plugins.push(
new webpack.optimize.CommonsChunkPlugin("lib",0)">"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
"app",0)">"lib"],0)">"/pages/app.html",minify: {
collapseWhitespace: true,collapseInlineTagWhitespace: true,removeRedundantAttributes: true,removeEmptyAttributes: true,removeScriptTypeAttributes: true,removeStyleLinkTypeAttributes: true,removeComments: true
}
})
);
图片路径处理、压缩、CssSprite
$ npm i url-loader image-webpack-loader --save-dev // 图片路径处理,压缩 config.module.loaders.push({ test: /.(?:jpg|gif|png|svg)$/,loaders: [ "url?limit=8000&name=img/[hash].[ext]",0)">"image-webpack" ] }); 雪碧图处理:webpack_auto_sprites 对文件使用 hash 命名,做强缓存 根据docs,在产出文件命名中加上 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("*",93)">function(req,res,next) {
res.header("Access-Control-Allow-Origin",0)">"*");
next();
});
// 具体接口设置
app.get("/api/test",res) {
res.send({ code: 200,data: "your data" });
});
var server = app.listen(3000,93)">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,93)">function(err) {
// 本次上传耗时
var timming = Date.now() - startTime;
if (err) {
console.log("error ",err);
console.log("upload fail -",file.remotePath);
} else {
console.log("upload success -",timming + "ms");
}
// 每次上传之后检测下是否还有文件需要上传,如果没有就关闭连接
if (__assets__.length === 0) {
client.end();
} else {
uploadFile();
}
});
}
// 发起连接
function connect(conf) {
if (!__connected__) {
client.connect(__conf__);
}
}
// 连接成功
client.on("ready",93)">function() {
__connected__ = true;
uploadFile(Date.now());
});
// 连接已关闭
client.on("close",93)">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",93)">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(),0)">"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,所以热更新、自动刷新还是可以正常使用
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |