批处理
前端现在在做项目的时候大多数遇到的都是单页面应用,但有时需要做多页面的时候,会把单页拿过来修改成多页面,如果代码多了,对单页或多页的配置可能会混乱,那么有没有更好的方式能把单页面和多页面不同的配置代码分开,能更清楚的分辩他们的区别,这里是利用 批处理
对前端构建进行部署 git 地址 目录分为三块
single // 单页代码
share // 共用代码
many // 多页代码
只需要用到 批处理
对其中两者进行合并就能生成想要的单页或多页应用,提示需要安装国内的 npm 淘宝镜像
如果未安装的需要自行修改 build.bat 里的命令行 call cnpm install
为call npm install
如下所示:
先选择存放路径,输入项目名,选择要生成的是单页还是多页
这里以单页为示例,其实就是简单的对文件进行复制,复制完成后会自动安装依赖
安装完依赖后还会自动运行项目 如上开启的项目端口为 8080
目录如下
webpack4 共同配置(share)
这里用到了最新的 webpack4.0,它简化了很多配置,多线程输出,更快的构建能力,大大提高了开发的效率
首先看下配置文件config.js
const path = require('path'),
config = {
// 开发环境配置
dev: {
port: 8080,
// 接口代理
proxyTable: {
'/v2': {
target: 'https://api.douban.com',
changeOrigin: true
},
},
},
// 生产环境配置
build: {
packName: 'myProjcet', // 项目打包后名称
outputPath: '', // 打包后项目输出路径
templatePath: 'template.html', //html 模版路径, 基于根路径下
htmlShortPath: '/', //html 文件输出路径, 基于 outputPath
resourcesPath: '', // 最终打包路径
resourcesShortPath: 'resources', // 资源目录 {packName}/resources
},
switchVal: {
to_rem: false, // 是否开启 px 转 rem
to_eslint: false, // 是否开启 eslint 语法检测
},
};
// 输出的目录
config.build.outputPath = path.resolve(__dirname, '../../dist/', config.build.packName);
// 最终输出目录项目存放路径
config.build.resourcesPath = path.join(config.build.outputPath, config.build.resourcesShortPath);
module.exports = config;
这里有开发环境下的接口代理,
生产环境的目录名称和路径
还有可选的是否转换页面字体为 rem
和eslint
语法检测 eslint
校验是默认的规则校验
它还有其它的三种通用规则
可根据自身喜好去设置
然后是 utils.js
工具方法
module.exports = {
/***
* 获取 src 一级目录
*/
getFiles() {const files = glob.sync('src/**/'),
arr = [];
files.forEach((filepath) => {let name = filepath.split('/')[1];
if (name) {arr.push(...[name]);
}
})
let obj = {};
if (arr.length) {[...new Set(arr)].map(item => {obj[`@${item}`] = path.join(__dirname, `../src/${item}`);
})
}
return obj
},
/**
* 多页面命名 获取每个多页对应的 js 名命名
* **/
getFileName() {let fileName = glob.sync('src/**/index.js');
entryArr = {};
fileName.forEach(function(path) {let arr = path.split('/');
let name = arr[arr.length - 2];
entryArr[name] = './' + path;
})
return entryArr;
},
/***
* 静态目录存放路径
*/
assetsPath(_path) {return path.posix.join(config.build.resourcesShortPath, _path);
},
copyDir(source, target) {rm('-rf', target);
mkdir('-p', target);
cp('-R', source, target);
}
}
再来看在开发和生产共用的代码 webpack.base.conf.js
首先看下一些基本的对 vue、css、js
这些 loader 的操作
rules: [{ test: /\.vue$/, loader: 'vue-loader',},
{test: /\.css$/, use: ['style-loader', 'css-loader'] },
{test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
{test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{
test: /\.js$/,
loader: !process.env.NODE_ENV ? 'happypack/loader?id=happy-babel' : 'babel-loader',
//loader: 'babel-loader',
exclude: /(node_modules|lib)/,
include: [ // 表示只解析以下目录,减少 loader 处理范围
path.resolve(__dirname, '../src'),
],
},
{test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: [{
loader: 'file-loader',
options: {
// 生产环境真实路径
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}]
},
{test: /\.(png|jpe?g|gif|svg|webp)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
// 生产环境真实路径
name: utils.assetsPath('image/[name].[hash:7].[ext]')
}
}
},
]
嗯都给了注释,要注意的是 css、less、scss
的 loader 顺序,不要写反因为他是从前往后这样编译的 如果找不到前面的后面的 loader 也就无法执行 js
的 loader 用了一段这个
!process.env.NODE_ENV ? 'happypack/loader?id=happy-babel' : 'babel-loader',
因为在生产环境下打包时js
loader 的编译会很慢,所以开启了多线程去处理js
loader 的编译
HappyPack = require('happypack'),
os = require('os'),
happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length }),
// 利用多线程解决 js loader 编译过程耗时 除 scss 无法使用 css、vue 都可使用 (webpack4 本来就是多线程)
createHappyPlugin = (id, loaders) => new HappyPack({
id: id,
loaders: loaders,
threadPool: happyThreadPool,
verbose: true, // 允许 HappyPack 输出日志
});
需要在 plugins
下加上下面这段
createHappyPlugin('happy-babel', [{
loader: 'babel-loader',
options: {
babelrc: true,
cacheDirectory: true // 启用缓存
}
}]),
happy-babel
就是找到上面 loader 的 id, 但因为 webpack4 本来就是多线程的,这样做可能多此一举,暂时没有测试过量大时编译效果
还有这个
new VueLoaderPlugin()
在 vue-loader
版本为 15.0 以后都要加上
其它在升级到 webpack4.0 后还是有不少的坑,
就比如 4 之前可用的合并加载文件
new webpack.optimize.MinChunkSizePlugin({minChunkSize: 30000}),
这个已经整合到 splitChunks
里面去了,再用的话就会冲突报错
因为之前没有留意 用 3 升 4 的过程中没有删除它,所以大家要重新配置 4 的时候还是重新一步步配置,否则很多报错都莫名其妙,接着往下看
if (!process.env.NODE_ENV) {for (let i = 1; i < 3; i++) {
// 使用 mini-css-extract-plugin 在生产环境要把 style-loader 覆盖,它们会有冲突
config.module.rules[i].use[0] = {loader: MiniCssExtractPlugin.loader,};
// 自动添加样式补全放
config.module.rules[i].use.splice(2, 0, 'postcss-loader');
}
//css 样式合并
config.plugins.push(
new MiniCssExtractPlugin({filename: utils.assetsPath('css/[name].[chunkhash:8].css'),
})
)
}
在 生产环境 下原来是用 ExtractTextPlugin
插件现在都改成了 MiniCssExtractPlugin
for 循环里面主要是把vue、css、less、scss
的第一个数组 style-loader
覆盖成 MiniCssExtractPlugin
否则会有冲突,
自动添加前缀的 postcss-loader
要放到最后面,这也是执行顺序的问题
在项目最外层要增加一个 postcss.config.js
内容是
module.exports = {
plugins: [require('autoprefixer')({browsers: ['last 20 versions']
})
]
}
require 的是一个自动补全 css 前缀的插件 last 20 versions
指的是兼容主流浏览器最近的 20 个版本,当然如果想要兼容到某个浏览器的特定版本也可以这样写
'last 10 Chrome versions',
'last 5 Firefox versions',
'Safari >= 6',
'ie> 8
接下来是前面提过的 px 转 rem
和eslint
语法检查,是否开启和关闭是在 config.js
里设置
build.js
是这里生产打包,操作都是先清空原来的输出目录, 复制静态文件到输出目录 然后打包
const spinner = ora("开始构建生产环境.....");
spinner.start();
// 清空输出目录
rm("-rf", config.build.outputPath);
// 复制 static 到输出目录
utils.copyDir("./static/*", config.build.resourcesPath);
webpack(webpackConfig).run((err, stats) => {spinner.stop();
if (err) throw err;
// 输出编译结果
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false,
timings: false
}) + "\n\n");
});
以上就是使用单页或多页共同的代码块
webpack4 单页配置(single)
单页应用的目录结构主要是这样的,和一般开发中的 vue
项目结构一样
build
--views
--index.html
--404.html
--build.js
--config.js
--dev-server.js
--utils.js
--webpack.base.conf.js
--webpack.dev.conf.js
--webpack.prod.conf.js
src
--conponents
--css
--font
--images
--mixins
--pages // 页面目录
--router
--store
--App.vue
--index.js
static
--jquery
mode_modules
看 build 里的配置文件,前面讲过了 build.js、config.js、utils.js、webpack.prod.conf.js
现在就先说下webpack.dev.conf.js
const webpackConfig = merge(baseWebpackConfig, {
mode: "development",
entry: ["webpack-hot-middleware/client?noInfo=true&reload=true"].concat("./src/index.js"),
devtool: "eval-source-map",
output: {
path: config.build.outputPath,
filename: "index.js",
},
module: {rules: []
},
resolve: {},
plugins: [new webpack.HotModuleReplacementPlugin(),
new Jarvis({port: 1337})
],
devServer: {inline: true},
});
webpack4.0 新增了一个 mode 为 development/production
, 两种模式在不同环境下都做了优化操作, 想要访问这两种模式还是需要用到process.env.NODE_ENV
关于页面热加载直接使用 webpack 自带的热加载功能 HotModuleReplacementPlugin
然后和入口文件 src/index.js
做一个合并
["webpack-hot-middleware/client?noInfo=true&reload=true"].concat("./src/index.js")
后面的 noinfo
和reload
是可配置的,如果想继续增加参数可往这里添加,传送门
然后开启热加载 devServer: {inline: true}
在output
里的 path 路径我指向的是打包输出路径,webpack 开发环境 是打包到内存的并不是真的打包,filename 是给了个固定的 index.js
这个是要写到 html
里做为整个项目的入口,也就是说整个项目运行就靠这个 index.js
,
在 plugins 里有一个 new Jarvis
这里的端口是 1337,项目运行后可以打开这个端口来看下文件大小,项目运行是否出错等等, 这个可视化窗口功能还不错,适合有双屏的同学
接下来看下webpack.prod.conf.js
const webpackConfig = {entry: { index: './src/index.js'}, //webpack4 默认会去查找./src/index.js
output: {
path: config.build.outputPath,
publicPath: '/',
filename: utils.assetsPath('js/[name].[chunkhash:8].js'),
chunkFilename: utils.assetsPath('js/[name].[chunkhash:8].js')
},
mode: 'production',
devtool: 'false',
module: {rules: [] },
optimization: {
runtimeChunk: { // 获取页面共同引用的代码
name: "manifest"
},
splitChunks: {
chunks: 'initial',
minChunks: 2,
maxInitialRequests: 5,
minSize: 30000, //
maxInitialRequests: 3,
automaticNameDelimiter: '~',
cacheGroups: {
vendors: {
name: 'vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
enforce: true,
},
default: {test: /[\\/]src[\\/]/,
priority: -20,
reuseExistingChunk: true
}
}
}
},
plugins: [
new WebpackBar({minimal: false,}),
new HtmlWebpackPlugin({filename: path.join(config.build.htmlShortPath, 'index.html'),
template: config.build.templatePath,
inject: true,
chunks: ['manifest', 'index'], // 引入 index.js
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: false
}
}),
//css 压缩
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorOptions: {safe: true, discardComments: { removeAll: true} },
canPrint: true
}),
]
};
这里说下在 output 下的 publicPath,如果要把打包后的文件指向一个相对路径要加上 /
要不然生成出来的的入口文件会变成 resources/js/xxx.js
而不是我们期待的 /resources/js/xxx.js
再则图片的路径也会变成 resources/image/...png
, 这样是无效的路径,当然这还是要看你用的是相对路径还是绝对路径了
来看下 optimization
这个东西,这是 webpack4 新加的功能用于代码的合并策略,这里是对两个地方的 js 进行合并一个是 npm 包一个是项目下的代码
cacheGroups: {
vendors: {
name: 'vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
enforce: true,
},
default: {test: /[\\/]src[\\/]/,
priority: -20,
reuseExistingChunk: true
}
}
这是符合合并规则条件的共同设置
chunks: 'initial',
minChunks: 2,
maxInitialRequests: 5,
minSize: 30000, //
maxInitialRequests: 3,
automaticNameDelimiter: '~',
也可以把他们拎到具体的合并对象下去做单独的规则设置
然后在 plugins
下引用上面的合并后的 js
new HtmlWebpackPlugin({filename: path.join(config.build.htmlShortPath, 'index.html'),
template: config.build.templatePath,
inject: true,
chunks: ['manifest', 'index'], // 引入 index.js
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: false
}
}),
chunks
它有如下三个模式, 可自行调整
- async 表示只从异步加载得模块(动态加载 import())里面进行拆分
- initial 表示只从入口模块进行拆分
- all 表示以上两者都包括
再看下 dev-server.js
启动项入口
let port = process.env.PORT || config.dev.port;
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true}));
app.engine('html', ejs.__express);
app.set('view engine', 'html');
app.set('views', path.resolve(__dirname, 'views'));
app.use(compression()); // 开启 gzip
//webpack 编译器
const compiler = webpack(webpackConfig);
//webpack-dev-server 中间件
const devMiddleware = require('webpack-dev-middleware')(compiler, {
// 这里必填 与 webpack 配置的路径相同
publicPath: webpackConfig.output.publicPath,
stats: {
colors: true,
chunks: false,
}
})
// 热更新中间件
const hotMiddleware = require('webpack-hot-middleware')(compiler);
// 处理本地开发环境下的代理接口
Object.keys(config.dev.proxyTable).forEach(function(context) {const options = config.dev.proxyTable[context];
if (typeof options === 'string') {
options = {target: options}
}
if (~context.indexOf(',')) {context = context.split(',');
}
app.use(proxyMiddleware(context, options));
})
app.use(devMiddleware);
app.use(hotMiddleware);
// 静态资源目录 指向 static 目录
app.use(express.static('./static'));
app.get('/*', function(req, res) {res.render('index');
});
// 无路由时跳转 404
app.get('*', function(req, res) {res.render('404');
})
app.listen(port, function() {console.log('node 启动 正在监听端口:', port)
})
这里利用 nodejs
调用模板进行页面渲染
app.set('views', path.resolve(__dirname, 'views'));
指向的是当前 build
下的 views
目录下的 html 文件,
开启热更新和开发接口代理
app.use(devMiddleware);
app.use(hotMiddleware);
app.use(express.static('./static'));
指向本地的静态资源
比如本地的图片路径是 /images/jpge.jpg
,
在开发环境下访问就会变成http://localhost:8080/static/images/jpge.jpg
,
app.get('/*', function(req, res) {res.render('index');
});
把所有路径直接指向到 views/index.html
下 文件内容如下
<body>
<!-- 开发环境 -->
<div id="app"></div>
<script type="text/javascript" src="lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="lib/bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="index.js"></script>
</body>
index.js
就是之前的入口文件,必须要写进 html 文件里的,因为没有用 HtmlWebpackPlugin
做模板的映射,当真正在开发环境下使用 (template.html)
模板是这样子的
<body>
<!-- 生产环境 -->
<div id="app"></div>
<script type="text/javascript" src="/resources/lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/resources/lib/bootstrap/js/bootstrap.min.js"></script>
</body>
所以分了两个模板去渲染页面
webpack4 多页配置(single)
多页应用的目录结构
build
--views
--index.html
--404.html
--build.js
--config.js
--dev-server.js
--utils.js
--webpack.base.conf.js
--webpack.dev.conf.js
--webpack.prod.conf.js
src
--conponents
--css
--font
--images
--mixins
--pages // 页面目录
--new
--index.js // 入口
--new.vue
static
--jquery
mode_modules
build 目录下有三个文件有些改动 dev-server.js
去掉了视图目录指向
因为是多页的,这里是获取 src 目录下的一级目录做为路由
// 这个获取的是内存路径
app.get('/:viewname?', function(req, res, next) {
var viewname = req.params.viewname ? req.params.viewname + '.html' : 'main.html';
var filepath = path.join(compiler.outputPath, viewname);
compiler.outputFileSystem.readFile(filepath, function(err, result) {if (err) {res.send('can\'t not find the file: ' + filepath).end;
return;
}
res.set('content-type', 'text/html');
res.send(result);
res.end();});
});
然后是 webpack.dev.conf.js
里加了这一段
let entryObj = utils.getFileName();
Object.keys(entryObj).forEach((name) => {webpackConfig.entry[name] = ['webpack-hot-middleware/client?noInfo=true&reload=true'].concat(entryObj[name]);
let plugin = new htmlWebpackPlugin({
filename: name + '.html',
template: config.build.templatePath,
inject: true,
chunks: [name]
});
webpackConfig.plugins.push(plugin);
})
获取 src 目录下的每个文件做为入口进行模板渲染
同样在 webpackprod.conf.js
也需要加上
let entryObj = utils.getFileName();
Object.keys(entryObj).forEach((name) => {webpackConfig.entry[name] = entryObj[name];
let plugin = new HtmlWebpackPlugin({chunks: ['manifest', name],
filename: name + '.html',
template: config.build.templatePath,
inject: true,
environment: 'resources',
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: false
}
});
webpackConfig.plugins.push(plugin);
})
这里多了一个 environment
他是插入模板的一个变量,为区分开发和生产环境路径
<body>
<!-- 生产环境 -->
<div id="app"></div>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.environment %>/lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.environment %>/lib/bootstrap/js/bootstrap.min.js"></script>
</body>