之前一直用的脚手架, 这次自己搭建 webpack 前端项目, 花费了不少心思, 于是做个总结.
1. 用法
项目结构如下:
- project
- |- bulid <!-- 这个目录是自动生成的 -->
- |- public
- |- CSS
- |- js
- |- page1.html <!-- 插件生成的 html 文件 -->
- |- page2.html <!-- 插件生成的 html 文件 -->
- ...
- |- public/ <!-- 存放字体, 图片, 网页模板等静态资源 -->
- |- src <!-- 源码文件夹 -->
- |- components/
- |- css/
- |- js/
- |- page1.js <!-- 每个页面唯一的 vue 实例, 需绑定到 #app-->
- |- page2.js <!-- 每个页面唯一的 VUE 实例, 需绑定到 #app-->
- ...
- |- package.json
- |- package-lock.json
- |- README.md
复制代码
public 文件夹存放一些静态文件, src 文件夹存放源码. 每个页面通过一个入口文件 (page1.js,page2.js,..) 生成 vue 实例, 挂载到插件生成的 html 文件的 #app 元素上.
安装依赖
$ npm install
复制代码
进入开发模式
$ npm run start
复制代码
浏览器会打开 http://localhost:3000, 这时页面一片空白, 显示 cannot get 几个字. 不要慌, 在 url 后面加上 /page1.html, 回车, 便可看见我们的页面. 这是因为我把开发服务器的主页设置为 index.html, 而本例中页面为 page1.html,page2.html, 因此会显示一片空白.
开发完成了, 构建生产版本:
$ npm run build
复制代码
这会产生一个 build / 文件夹, 里面的文件都经过优化, 服务器响应的资源, 就是来自于这个文件夹.
2. 介绍
2.1 webpack 基础配置
我们的开发分为生产环境和开发环境, 因此需要有 2 份 webpack 的配置文件(可能你会想用 env 环境变量, 然后用 3 目运算符根据 env 的值返回不同值. 然而这种方法在 webpack 导出模块的属性中无效, 我试过~~~). 这里我们拆分成 3 个文件, 其中 webpack.common.js 是常规的配置, 在两种环境下都会用到, webpack.dev.js 和 webpack.prod.js 则是在 2 种环境下的特有配置. 这里用到 webpack-merge 这个包, 将公共配置和特有配置进行合成.
- webpack.common.js
- const path = require('path');
- const HtmlWebpackPlugin = require('html-webpack-plugin');
- const VueLoaderPlugin = require('vue-loader/lib/plugin');
- const devMode = process.env.NODE_ENV !=='production';
- // 需要被打包入口文件数组
- // 数组元素类型 {string|object}
- // string: 将以默认规则生成 bundle
- // object{filename|title|template} 生成的 bundle.html 的文件名 | title 标签内容 | 路径 /public 下的模板文件(需指定文件后缀)
- const entryList = [
- 'page1',
- 'page2',
- ];
- /**
- * @param {array} entryList
- * @param {object} option: 可选 要手动配置的内容
- */
- const createEntry = (list = [], option = {}) => {
- const obj = {};
- list.forEach((item) => {
- const name = item.filename ? `./js/${item.filename}` : `./js/${item}`;
- obj[name] = path.resolve(__dirname, './src', `./${item}.js`);
- });
- return Object.assign(obj, option);
- };
- module.exports = {
- entry: createEntry(entryList),
- output: {
- path: path.resolve(__dirname, './build'),
- },
- module: {
- rules: [
- {
- test: /\.js$/,
- exclude: /(node_modules|bower_components)/,
- use: {
- loader: 'babel-loader',
- options: {
- presets: ['@babel/preset-env'],
- },
- },
- },
- {
- test: /\.vue$/,
- use: 'vue-loader',
- },
- {
- test: /\.(woff|woff2|eot|ttf|otf)$/,
- use: {
- loader: 'file-loader',
- options: {
- name: 'public/fonts/[name].[ext]',
- },
- },
- },
- {
- test: /\.(png|svg|jpg|gif)$/,
- use: {
- loader: 'file-loader',
- options: {
- name: 'public/images/[name].[ext]',
- },
- },
- },
- ],
- },
- plugins: createPluginInstance(entryList).concat([
- // vue SFCs 单文件支持
- new VueLoaderPlugin(),
- ]),
- };
复制代码
这里我们没有进行 css 文件的配置, 是因为生产环境下需要优化, 提取, 所以在另外 2 个文件分别配置.
- webpack.dev.js
- const webpack = require('webpack');
- const path = require('path');
- const merge = require('webpack-merge');
- const common = require('./webpack.common.js');
- module.exports = merge(common, {
- mode: 'development',
- devtool: 'inline-source-map',
- output: {
- filename: '[name].js',
- chunkFilename: '[name].js',
- },
- module: {
- rules: [
- {
- test: /\.(css|less)$/,
- use: [
- 'vue-style-loader',
- 'css-loader',
- 'postcss-loader',
- 'less-loader'
- ],
- },
- ],
- },
- resolve: { alias: { vue: 'vue/dist/vue.js' } },
- });
复制代码
vue 分为开发版本和生产版本, 最后一行是根据路径指定使用哪个版本.
- webpack.prod.js
- const webpack = require('webpack');
- const merge = require('webpack-merge');
- const CleanWebpackPlugin = require('clean-webpack-plugin');
- const MiniCssExtractPlugin = require("mini-css-extract-plugin");
- const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
- const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
- const common = require('./webpack.common.js');
- module.exports = merge(common, {
- mode: 'production',
- output: {
- filename: '[name].[contenthash].js',
- chunkFilename: '[name].[contenthash].js',
- },
- resolve: { alias: { vue: 'vue/dist/vue.min.js' } },
- module: {
- rules: [
- {
- test: /\.(css|less)$/,
- use: [
- MiniCssExtractPlugin.loader,
- 'css-loader',
- 'postcss-loader',
- 'less-loader'
- ],
- },
- ],
- },
复制代码
在 production 环境下, 我们使用了哈希值便于缓存 https://webpack.js.org/guides/caching/ , 以后往生产环境下添加其他资源都会如此.
2.2 解决文件输出目录
我们期待的 build 文件夹具有如下结构:
- build
- |- css/
- |- js/
- |- page1.html
- |- page2.html
- ...
复制代码
即文件按照类型放在一起, html 文件直接放在该目录下, 可是我们上面的配置的输出结果是混合在一起的. 由于 name 属性既可以是文件名, 也可以是 / dir/a 之类带有路径的文件名, 我们根据这个特点做出一些修改.
直接对 output 的输出路径更改
比如改为 build/js, 其他资源利用相对路径比如../page1.html 进行修改. 我一开始就这样做的, 但最终会导致开发服务器无法响应文件的变化, 因为他只能针对输出目录下的文件进行监听, 该目录之上的文件变化无能为力.
修改入口名称
这也是我们的最终解决方案. 将原来的文件名 page1 修改为 / js/page1, 最终输出的 js 文件便都会放在 js 文件夹里. 在生产环境下我们通过 MiniCssExtractPlugin 这个插件提取 js 文件中的 css, 这是该插件的配置:
- new MiniCssExtractPlugin({
- filename:'[name].[contenthash].css'
- })
复制代码
这里的 name 就是当初入口的名字, 受到入口名称更改的影响, 上面最终会变成 js/page1.131de8553ft82.css, 并且该占位符 [name] 只在编译时有效, 这意味着无法用函数对该值进行处理. 因此不能使用 [name] 占位符达到想要的目的, 干脆只用[id].
- new MiniCssExtractPlugin({
- filename:'/css/[id].[contenthash].css'
- })
复制代码
3. 代码分割
在 webpack4 中使用 optimization.splitChunks 进行分割.
- //webpack.common.js
- const path = require('path');
- module.exports = {
- // ... 省略其他内容
- optimization:{
- runtimeChunk:{
- name:'./js/runtime'
- },
- splitChunks:{
- // 避免过度分割, 设置尺寸不小于 30kb
- //cacheGroups 会继承这个值
- minSize:30000,
- cacheGroups:{
- //vue 相关框架
- main:{
- test: /[\\/]node_modules[\\/]vue[\\/]/,
- name: './js/main',
- chunks:'all'
- },
- // 除 Vue 之外其他框架
- vendors:{
- test:/[\\/]node_modules[\\/]?!(vue)[\\/]/,
- name: './js/vendors',
- chunks:'all'
- },
- // 业务中可复用的 js
- extractedJS:{
- test:/[\\/]src[\\/].+\.js$/,
- name:'./js/extractedJS',
- chunks:'all'
- }
- }
- }
- }
- };
复制代码
runtimeChunk 包含了一些 webapck 的样板文件, 使得你在不改变源文件内容的情况下打包, 哈希值仍然改变, 因此我们把他单独提取出来, 点这儿 https://webpack.js.org/guides/caching/ 了解更多. cacheGroups 用于提取复用的模块, test 会尝试匹配 (模块的绝对路径 || 模块名), 返回值为 true 且满足条件的模块会被分割. 满足的条件可自定义, 比如模块最小应该多大尺寸, 至少被导入进多少个 chunk(即复用的次数) 等. 默认在打包前模块不小于 30kb 才被会分割.
4. 树抖动
在 package.json 里加入
"sideEffects":["*.css","*.less","*.sass"]
复制代码
该数组之外的文件将会受到树抖动 https://webpack.js.org/guides/tree-shaking/ 的影响 -- 未使用的代码将会从 export 导出对象中剔除. 这将大大减少无用代码. 如果树抖动对某些文件具有副作用, 就把这些文件名放进数组以跳过此操作. css 文件 (包括. less,.sass) 都必须放进来, 否则会出现样式丢失.
5. 插件的使用
5.1 clean-webpack-plugin
每次打包后都会生成新的文件, 这可能会导致无用的旧文件堆积, 对于这些无用文件自己一个个删太麻烦, 这个插件会在每次打包前自动清理. 实际中, 我们不想在开发环境下清理掉 build 命令生成的文件, 因此只在生产环境使用了这个插件.
5.2 html-Webpack-plugin
我们的源码目录中并没有 html 文件, 打包后的多个 html 文件, 就是我们用这个插件生成的.
- //webpack.common.js
- // ... 省略上面已经出现过的内容
- // 每个 html 需要一个插件实例
- // 批量生成 html 文件
- const createPluginInstance = (list = []) => (
- list.map((item) => {
- return new HtmlWebpackPlugin({
- filename: item.filename ? `${item.filename}.html` : `${item}.html`,
- template: item.template ? `./public/${item.template}` : './public/template.html',
- title: item.title ? item.title : item,
- chunks: [
- `./js/${item.filename ? item.filename : item}`,
- './js/extractedJS',
- './js/vendors',
- './js/main',
- './js/runtime',
- './css/styles.css',
- devMode ? './css/[id].css' : './css/[id].[contenthash].css',
- ],
- });
- })
- );
复制代码
默认会将所有的入口文件, 代码分割后的文件打包进一个 html 文件里, 通过指定 chunks 属性来告诉插件只包含哪些块, 或者 exludeChunks 指定不应包含那些 chunks. 这里有个小问题, 我们无法让文件刚好只包含他需要的块. 若想不包含未使用的 chunks, 只能根据实际情况手动配置, 用这个函数批量生成的文件, 总会包含所有的公共打包文件.
5.3 mini-css-extract-plugin (prooduction)
该插件用于提取 js 文件中的 css 到单独的 css 文件中.
- //webpack.prod.js
- //... 省略其他内容
- plugins:[
- new CleanWebpackPlugin('build'),
- // 提取 css
- new MiniCssExtractPlugin({
- filename:'./css/[id].[contenthash].css'
- }),
- // 优化缓存
- new webpack.HashedModuleIdsPlugin()
- ]
复制代码
5.4 optimize-css-assets-webpack-plugin (production)
用于精简打包后的 css 代码, 设置在配置 optimization 的 minimizer 属性中, 这将会覆盖 webpack 默认设置, 因此也要同时设置 js 的精简工具(这里我们用 uglifyplugin 插件):
- optimization: {
- minimizer:[
- new UglifyJsPlugin({
- cache: true,
- parallel: true
- }),
- new OptimizeCSSAssetsPlugin()
- ]
- }
复制代码
6. 开发服务器, 热模块替换 (development)
webpack.dev.js 中增加如下内容即可:
- //... 省略其他内容
- devServer:{
- index:'index.html',
- hot:true,
- contentBase:path.resolve(__dirname,'./build'),
- port:3000,
- noInfo:true
- },
- plugins:[
- new webpack.HotModuleReplacementPlugin()
- ]
复制代码
使用开发服务器可以在我们修改了源文件后自动刷新, 因为是将数据放在内存中, 因此不会影响硬盘中 build 文件夹. 热模块替换还需要在源文件做相应修改. 我们也为动态导入语法进行了相应配置.
7. 其他
public 用于存放静态资源, 打包后也会在 build / 下创建一个同名文件夹, 里面存放的是 public 会被使用到的资源. 如果在. css 文件里引用了 public 里的资源, 如图片, 添加 url 的时候要使用绝对路径:
- <!-- src/css/page1.css -->
- .bg-img {
- background-image:url(/public/images/1.jpg)
- }
复制代码
这样通过 http/https 打开的时候就能正常使用, 如果是以文件形式打开(比如打包后双击 page1.html), 会发现浏览器显示无法找到资源. 通过导入图片作为变量引用(import name from path), 既可使用绝对路径, 也可使用相对路径.
有什么错误, 欢迎指正, 如果你喜欢, 可以点赞 https://github.com/comWang/frontend-project-build .
来源: https://juejin.im/post/5b9b4f046fb9a05d37618115