最近,发现自己离前端新技术接触越来越少了,由于用不上,连以前接触过的webpack、react这些都开始淡忘。本着一颗学习的心,打算在工作之余,抽取一部分时间来钻研,搭建一个简单的管理平台。
基于技术上的发展、稳定性及目前提倡的自动化和模块化开发,本人暂时确定采用webpack/express/react/react-router/redux。
前端自动化和模块化 -- 前端工具webpack的使用
- 在开始之前,先看下项目的结构(由于项目采用的是express脚手架来生成的,后续添加的内容基本以此为基准)
public文件夹为前端开发目录,dist为打包后目录。
webpack配置。
- views中的html采用的是插件HtmlWebpackPlugin生成的
// 遍历所有模板中的.html文件,使用HtmlWebpackPlugin引入静态资源(或者用ejs模板生成多个)
var htmlfiles = fs.readdirSync(path.resolve(__dirname, "./src/public/templates"));
htmlfiles.forEach(function(item) {
var currentpath = path.resolve(__dirname+"/src/public/templates", item);
var extname = path.extname(currentpath);
if(fs.statSync(currentpath).isFile() && (extname == ".html" || extname == ".ejs")) {
var json = {
template: currentpath, // 设置引入的模板
filename: path.join(__dirname, './src/views/'+item), // 设置输出路径
inject: 'body', // 设置js插入位置
hash: true, // 为所有的资源加上hash值
chunks: ["style", "vendor1", "vendor2", "common", "chunk", "bundle"], // 指定引入的资源
showErrors: true // 是否展示错误
}
// 生产环境压缩html
if(isPro) {
json["minify"] = {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
}
}
config.plugins.push(
new HtmlWebpackPlugin(json)
);
}
});
- 设置环境变量process.env.NODE_ENV
// 在package.json中设置
"start": "set NODE_ENV=development && supervisor -i ./src/public ./src/bin/www"
- 采用ExtractTextPlugin和postcss-loader处理样式。在js中采用require引入css样式表,webpack默认将其合并到编译后的文件中,故需要将其抽取出来,做转换和兼容处理。
## 在loader中添加
{
// 识别js中require引入的样式表,并将其转换和兼容处理
test: /\.(css|scss|sass)$/,
exclude: /node_modules/,
use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: { autoprefixer: true, sourceMap: true, importLoaders: 1 }
},
{
loader: 'postcss-loader',
options: {
sourceMap: true,
plugins: () => [autoprefixer({ browsers: ['last 2 versions', 'safari >= 5', 'ie 8', 'ie 9','iOS >= 7', 'Android >= 4.1'] })],
}
},
'sass-loader'
]
}))
}
## 在plugins中添加
// 抽离样式
new ExtractTextPlugin({
filename: (getPath) => {
return getPath('css/style.css');
},
allChunks: true
}),
- 用加载器url-loader对图片进行处理
{
// 图片加载器,可以将较小的图片转成base64,减少http请求
// 如下配置,将小于8kb的图片转成base64码,大于8192byte的图片将通过file-loader把源文件迁移到指定的路径,并返回新的路径
// [hash]/[name] hash值,防止重名,如两张图片一样,只会生成同一hash
test: /\.(png|jpg|gif|svg)$/,
loader: [
'url-loader?limit=8192&name=images/[hash].[ext]',
'image-webpack-loader'
],
- 对js进行转换处理
## loader中添加
// 在新版中,loaders中的加载器,不能以"es3ify!babel"这样连起来,要么数组,要么"es3ify-loader!babel-loader"
{
// es3ify-loader 兼容ie8,将es5转译成es3
test: /\.js$/,
exclude: /node_modules/,
loaders: ["es3ify-loader", "babel-loader"]
}
- 设置自动补全文件后缀
resolve: {
extensions: ['*', '.js', '.jsx', ".css", ".scss"] // 值得注意的是,在新版中,数组中不能存在'',而是用'*'替代
}
- 使用插件CommonsChunkPlugin抽取公共依赖模块
new webpack.optimize.CommonsChunkPlugin({
name: 'chunk',
chunks: ['vendor2', 'bundle'] // 这里是output生成的输出文件
}),
- 使用插件CopyWebpackPlugin来拷贝资源,主要用于第三方插件/库的拷贝,配合webpack的externals配置,可直接在html中引用cdn/本地第三方插件/库,而不会被webpack打包
// 拷贝资源
new CopyWebpackPlugin(
[{
from: './src/public/plugins/',
to: 'plugins'
}]
)
- 使用插件UglifyJsPlugin压缩代码
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
mangle: {
except: ['jQuery', '$', 'exports', 'require', "module"]
}
})
- noParse、alias和ProvidePlugin搭配
// 设置引入的插件别名,重定向到指定的文件,阻止webpack对require引用的文件进行遍历,查询其依赖等
alias: {
react : path.join(__dirname, "node_modules/react/dist/react.min.js"),
"react-dom" : path.join(__dirname, "node_modules/react-dom/dist/react-dom.min.js")
}
// 设置自动引入相应模块功能
// 当脚本有引入以下变量时,会自动加载引入相对应的模块
new webpack.ProvidePlugin({
React: 'react',
ReactDOM: 'react-dom'
})
// 默认的require()方法会在webpack打包的时候去解压,遍历ReactJS及其依赖,使用noParse可阻止其默认行为
noParse: [
path.join(__dirname, "node_modules/react/dist/react.min.js"),
path.join(__dirname, "node_modules/react-dom/dist/react-dom.min.js")
]
热更新
作为一个会偷懒的前端,使用到了webpack,怎么能漏掉其热更新功能呢。webpack的热更新功能目前主要有两种配置方式,一种为webpack-dev-server,另一种为webpack-dev-middleware和webpack-hot-middleware两个中间件配合。其实webpack-dev-server也是采用webpack-dev-middleware这个中间件来实现的。由于本人后端采用的是node,直接忽略掉了webpack-dev-server。
- 先装个逼
cnpm intstall webpack-dev-middleware --save-dev
cnpm intstall webpack-hot-middleware --save-dev
- 配置webpack.config.js
var publicPath = "http://127.0.0.1:3000/dist/"; // output中设置publicPath且publicPath必须为绝对路径
var webpackHotMiddleware = 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=10000&reload=true';
entry: {
vendor1 : ["es5-shim", "es5-sham", "console-polyfill", "babel-polyfill", webpackHotMiddleware],
vendor2 : ["jquery", "react", "react-dom", webpackHotMiddleware],
common: ["commonExt", webpackHotMiddleware],
bundle : ["register", webpackHotMiddleware]
},
output: {
path: path.resolve(__dirname, './src/dist'),
publicPath:publicPath,
filename: 'js/[name].js',
}
- 配置app.js(express的配置文件)
// 最好将其放在路由配置之前
if(process.env.NODE_ENV == "production") {
app.use(express.static(path.join(__dirname, 'dist'),{maxAge:1000*60*60*30}));
app.use(express.static(path.join(__dirname, '../src'),{maxAge:1000*60*60*30}));
app.use(express.static(path.join(__dirname, 'tmp'),{maxAge:1000*60*60*30}));
}else{
var webpack = require("webpack");
let devMiddleWare = require('webpack-dev-middleware');
let hotMiddleWare = require('webpack-hot-middleware');
let webpackconfig = require('../webpack.config.js');
var compiler = webpack(webpackconfig);
app.use(devMiddleWare(compiler,{
publicPath: webpackconfig.output.publicPath,
noInfo: true,
lazy: false,
stats: {
colors: true,
chunks: false
}
}));
- 在入口文件上添加
// 如我的入口文件为register.js
if (module.hot) {
module.hot.accept();
}
- 以上配置只能实现js的热更新,css要实现实时刷新效果,就必须引入css-hot-loader
结语
前奏基本已经搞定,剩下的就是一些优化了,不得不感慨前端工具发展之快,从grunt到gulp再到webpack,前端的打包工具不断的进化,改朝换代的速度让人不得不感慨。每种工具都学点,也懒得深入研究,一路挖坑一路填坑,回头想想,擦,谁的锅!!!
附上本人的完整webpack配置
var path = require("path");
var webpack = require("webpack");
var fs = require("fs");
var autoprefixer = require("autoprefixer");
var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); // 提取公共部分插件
var HtmlWebpackPlugin = require('html-webpack-plugin'); // 自动生成index.html页面插件
var CopyWebpackPlugin = require('copy-webpack-plugin'); // 拷贝资源插件
var ExtractTextPlugin = require("extract-text-webpack-plugin"); // 抽离css样式,通过require引入的css,会被webpack打包到js中
var ENV = process.env.npm_lifecycle_event; // 直接通过获取启动命令来判断开发/生产环境
var isPro = process.env.NODE_ENV == "production";
var publicPath = "http://127.0.0.1:3000/dist/";
var webpackHotMiddleware = 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=10000&reload=true';
var config = {
entry: {
vendor1 : ["es5-shim", "es5-sham", "console-polyfill", "babel-polyfill", webpackHotMiddleware],
vendor2 : ["jquery", "react", "react-dom", webpackHotMiddleware],
common: ["commonExt", webpackHotMiddleware],
bundle : ["register", webpackHotMiddleware]
},
output: {
path: path.resolve(__dirname, './src/dist'),
publicPath:publicPath,
filename: 'js/[name].js',
},
externals: {
// jQuery: 'window.$',
// 'react': 'React',
// 'react-dom': 'ReactDOM'
},
// devtool: 'eval-source-map',
module: {
// 默认的require()方法会在webpack打包的时候去解压,遍历ReactJS及其依赖,
noParse: [
path.join(__dirname, "node_modules/react/dist/react.min.js"),
path.join(__dirname, "node_modules/react-dom/dist/react-dom.min.js"),
path.join(__dirname, "node_modules/jquery/dist/jquery.min.js"),
path.join(__dirname, "node_modules/babel-polyfill/dist/polyfill.min.js"),
path.join(__dirname, "node_modules/console-polyfill/index.js"),
path.join(__dirname, "node_modules/es5-shim/es5-shim.min.js"),
path.join(__dirname, "node_modules/es5-shim/es5-sham.min.js")
],
loaders: [
{
// es3ify-loader 兼容ie8,将es5转译成es3
test: /\.js$/,
exclude: /node_modules/,
loaders: ["es3ify-loader", "babel-loader"]
},
{
// 识别js中require引入的样式表,并将其转换和兼容处理
test: /\.(css|scss|sass)$/,
exclude: /node_modules/,
use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: { autoprefixer: true, sourceMap: true, importLoaders: 1 }
},
{
loader: 'postcss-loader',
options: {
sourceMap: true,
plugins: () => [autoprefixer({ browsers: ['last 2 versions', 'safari >= 5', 'ie 8', 'ie 9','iOS >= 7', 'Android >= 4.1'] })],
}
},
'sass-loader'
]
}))
},
{
// 图片加载器,可以将较小的图片转成base64,减少http请求
// 如下配置,将小于8kb的图片转成base64码,大于8192byte的图片将通过file-loader把源文件迁移到指定的路径,并返回新的路径
// [hash]/[name] hash值,防止重名,如两张图片一样,只会生成同一hash
test: /\.(png|jpg|gif|svg)$/,
loader: [
'url-loader?limit=8192&name=images/[hash].[ext]',
'image-webpack-loader'
],
},
{
test: /\.(woff|woff2|eot|ttf)$/,
loader: 'file-loader?name=fonts/[name].[ext]',
},
]
},
resolve: {
extensions: ['*', '.js', '.jsx', ".css", ".scss"], // 用于自行补全文件后缀
alias: {
// 设置引入的插件别名或配置路径
src : path.resolve(__dirname, "./src"),
public : path.resolve(__dirname, "./src/public"),
components: path.resolve(__dirname, "./src/public/components"),
react : path.join(__dirname, "node_modules/react/dist/react.min.js"),
"react-dom" : path.join(__dirname, "node_modules/react-dom/dist/react-dom.min.js"),
"babel-polyfill" : path.join(__dirname, "node_modules/babel-polyfill/dist/polyfill.min.js"),
"console-polyfill" : path.join(__dirname, "node_modules/console-polyfill/index.js"),
"es5-shim" : path.join(__dirname, "node_modules/es5-shim/es5-shim.min.js"),
"es5-sham" : path.join(__dirname, "node_modules/es5-shim/es5-sham.min.js"),
jquery : path.join(__dirname, "node_modules/jquery/dist/jquery.min.js"),
commonExt: path.resolve(__dirname, "./src/public/js/commonExt"),
register : path.resolve(__dirname, './src/public/components/register.js')
}
},
plugins: [
// 配置全局标识
new webpack.DefinePlugin({
"process.env": { NODE_ENV: JSON.stringify(process.env.NODE_ENV || "development") }
}),
new webpack.LoaderOptionsPlugin({
debug: !isPro
}),
// 当脚本有引入以下变量时,会自动加载引入相对应的模块
new webpack.ProvidePlugin({
$: "jquery",// 一定要安装低于2.0版本的,否则ie8会报错
jQuery: "jquery",
"window.jQuery": "jquery",
React: 'react',
ReactDOM: 'react-dom'
}),
// 抽离样式
new ExtractTextPlugin({
filename: (getPath) => {
return getPath('css/style.css');
},
allChunks: true
}),
// 拷贝资源
new CopyWebpackPlugin(
[{
from: './src/public/plugins/',
to: 'plugins'
}]
),
// 提取被引用两次以上的公共部分
// new CommonsChunkPlugin({
// name:"chunk",
// minChunks:2,
// // minChunks: Infinity //提取所有entry依赖模块
// }),
new webpack.optimize.CommonsChunkPlugin({
name: 'chunk',
chunks: ['vendor2', 'bundle']
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
],
};
// 遍历所有模板中的.html文件,使用HtmlWebpackPlugin引入静态资源(或者用ejs模板生成多个)
var htmlfiles = fs.readdirSync(path.resolve(__dirname, "./src/public/templates"));
htmlfiles.forEach(function(item) {
var currentpath = path.resolve(__dirname+"/src/public/templates", item);
var extname = path.extname(currentpath);
if(fs.statSync(currentpath).isFile() && (extname == ".html" || extname == ".ejs")) {
var json = {
template: currentpath, // 设置引入的模板
filename: path.join(__dirname, './src/views/'+item), // 设置输出路径
inject: 'body', // 设置js插入位置
hash: true, // 为所有的资源加上hash值
chunks: ["style", "vendor1", "vendor2", "common", "chunk", "bundle"], // 指定引入的资源
showErrors: true // 是否展示错误
}
// 生产环境压缩html
if(isPro) {
json["minify"] = {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
}
}
config.plugins.push(
new HtmlWebpackPlugin(json)
);
}
});
if(isPro) {
config.plugins.push(
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
mangle: {
except: ['jQuery', '$', 'exports', 'require', "module"]
}
})
);
}
module.exports = config;