webpack折腾记(二)

最近很忙很忙,没法描述的那种忙:一个人搭后台,接webview,写活动页面...要不是之前简单折腾了下webpack,估计现在更是忙的不可开交。在此期间遇见了许多对于开发环境的需求(貌似webpack3也出来了),因此可以继续折腾折腾。

<!--more-->

这里先列举我遇见的需求及对应的解决方案,然后参考vue-cli的配置从头搭建一个较完善的开发环境模板。

1. 常见需求

下面的描述是一个关于APP分享的项目需求:

  • APP分享由web页面实现:APP内部由一个关于分享的介绍页面,然后分享链接打开有一个下载页面,除此之外还有一个具体业务的分享页面
  • 具体业务分享页面和前面两个页面的UI差别较大,样式无法复用且有独立的业务逻辑
  • 打包的页面需要接到后台模板(现在的后台是Laravel ,因此需要打包成xxx.blade.php

虽然需求不复杂,但是为这个需求搭建一个合适的开发环境却遇见了下面的这些问题(自己给自己找事~),下面来各个击破。

1.1. 打包多个文件

html页面打包成blade模板,可以使用,可以使用html-webpack-plugin插件来实现。将多个入口文件打包成多个出口文件,然后在对应的输出模板上加载对应的出口文件,这个需求相对要复杂一点,我们先将需求拆分成:

  • 将多个入口文件打包成多个输入文件,比如将page1.jspage2.js输出为bundle1.jsbundle2.js
  • 输出多个模板文件,比如讲page1.htmlpage2.html输出为page1.blade.phppage2.blade.php
  • 控制每个模板文件中加载的模块
  • 实现我们最终多对多的需求

步骤一

针对第一点,我们可以使用entry配置项的对象形式,,实现起来就很简单了

entry: {
    page1: `${DEMO_DIR}/page1.js`,
    page2: `${DEMO_DIR}/page2.js`
},
output: {
    path: OUTPUT_DIR,
    filename: "[name].js"
},

注意output.filename[name],这是输出文件名一个特殊的用法,对应的是入口文件名,这里就是我们配置的page1page2。除此之外还有[id],[hash]等用法,这里就不展开了,感兴趣的同学可移步这里

步骤二

输出多个模板文件就更简单了,html-webpack-plugin插件本身就支持输出多个文件,通过构造参数的templatefilename属性指定模板文件和输出文件即可。

plugins: [
    new HtmlWebpackPlugin({
        template: `${DEMO_DIR}/tpl/page1.html`,
        filename: "page1.html",
    }),
    new HtmlWebpackPlugin({
        template: `${DEMO_DIR}/tpl/page2.html`,
        filename: "page2.html",
    }),
]

接着步骤一的配置进行打包,可以发现在OUTPUT_DIR下生成了page1.htmlpage2.html,貌似我们的目的已经达到了。不行的是打开文件可以发现,每个模板都将page1.jspage2.js引入了,这就出事情了,打包的目的不就是减少加载文件的数量吗?引入多个文件不如将他们打包成一个文件呢~

步骤三

要解决这个问题也很容易,HtmlWebpackPlugin还支持chunks的配置,该属性接受一个数组表示需要引入的输出模块,在步骤二的基础上稍作修改

new HtmlWebpackPlugin({
    template: `${DEMO_DIR}/tpl/page1.html`,
    filename: "page1.html",
    chunks: ["page1"],
}),
new HtmlWebpackPlugin({
    template: `${DEMO_DIR}/tpl/page2.html`,
    filename: "page2.html",
    chunks:  ["page2"]
}),

现在继续打包,可以发现每个模板都只加载了对应的模块,所以官方文档才是最好的教程啊。

需要注意的是,这里的chunks对应的模块名,指的是entry配置中模块的属性名(这里只是恰好属性名与文件名相同了),希望不要造成歧义~

步骤四

咦?我们的需求貌似已经实现了~等等,是不是忘记了什么...对了,样式表!在各自的入口文件引入对应的scss文件,然后使用extract-text-webpack-plugin插件将样式表进行打包的文件中进行分离,问题是:如何分离多个样式表文件呢?

跟使用多次HtmlWebpackPlugin插件类似,我们可以定义多个ExtractTextPlugin对象,然后在rules中针对样式表文件定义更加详细的规则。由于使用ExtractTextPlugin本身的过程就要设置几个步骤,这里要稍微绕一点点 。

// 首先定义多个实例
let page1ExtractTextPlugin = new ExtractTextPlugin({
    filename: "page1.css"
});
let page2ExtractTextPlugin = new ExtractTextPlugin({
    filename: "page2.css"
});

// 针对具体的样式表指定其分离规则
module: {
    rules: [
        {
            test: /page1.scss$/,
            use: page1ExtractTextPlugin.extract({
                use: ["css-loader", "autoprefixer-loader", "sass-loader"],
            })
        },
        {
            test: /page2.scss$/,
            use: page2ExtractTextPlugin.extract({
                use: ["css-loader", "autoprefixer-loader", "sass-loader"],
            })
        }
    ]
},

// 记得在plugins中调用,不然会报错哦
plugins: [
    page1ExtractTextPlugin,
    page2ExtractTextPlugin,
]

打包可以看见,我们的目的已经达到了,由于是在不同的入口文件中加载了不同的样式表,这些样式表都会自动注入到对应的模板上,而不需要我们的chunks属性中去指定。这里也可以理解为webpack的核心是围绕着入口文件来工作的。

尽管打包多个文件的需求貌似已经完成了,但是可以看见,上面的配置明显比较冗余,接下来搭建的webpack模板就需要解决这个问题,在此之前,再看一看其他的常见需求。

1.2. 内联文件

把CSS代码放在JS文件里面很明显不是一个明智的做法,但是单独提一个样式表出来有时候也没有必要(比如一个独立的活动介绍页面,生命周期只有两三天),此外为了加载效率,将样式表内联貌似还不错。 那么,如何实现样式表的内联呢?

这里需要用到基于HtmlWebpackPlugin的插件html-webpack-inline-source-plugin。 该插件可以为HtmlWebpackPlugin拓展一个inlineSource的配置,该参数接收一个正则表达式形式的字符串,用于将匹配的文件类型内联到模板上。

let HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');

// 在插件中引入 HtmlWebpackInlineSourcePlugin,此时 HtmlWebpackPlugin 就会多一个 inlineSource 的配置项
plugins: [
    new HtmlWebpackInlineSourcePlugin(),
    new HtmlWebpackPlugin({
        template: `${DEMO_DIR}/tpl/page1.html`,
        filename: "page1.html",
        chunks: ["page1"],
        inlineSource: ".(css)quot;
    }),
]

inlineSource的参数修改为".(js|css)quot;,就可以将JS和CSS文件文件都内联到模板上面(但是由于JS文件需要babel转义,因此内联到页面上并不是很合适),内联文件更适合那种只有样式的静态页面,使用内联还可以减少将文件上传到CDN上的工作量呢。

1.3. 模板变量

貌似上面的工作都是围绕着页面模板来转的。这是理所应当的,页面是前端的基础嘛。实际上HtmlWebpackPlugin插件对于模板的支持远远超过了我们的想象,比如,我们可以使用直接使用ejs模板来代替纯HTML模板,这意味着,我们可以使用函数,变量等一系列语法特性来更快速地完成我们的工作。

先来验证一下这是不是真的

// page1.ejs
<h1>page1</h1>
<% var names = ["aaa", "bbb", "ccc"]; %>
<% if (names.length) { %>
<ul>
    <% names.forEach(function(name){ %>
    <li class="<%= name  %>"><%= name %></li>
    <% }) %>
</ul>
<% } %>

// 记得修改 HtmlWebpackPlugin 的 template 配置参数 

打包,走你~发现模板真的生成了对应得三个li标签,这对于编写模板时增加测试数据和填充内容还是很有好处的。关于更多ejs语法的问题,请移步这里

针对单个模板文件,使用ejs可以帮助我们快速生成页面;而针对同个项目的多个模板页面所需要的相同数据,比如相同的CDN路径前缀,我们该怎么处理呢?

跟据"DRY"原则,在每个模板文件上定义相同的变量可不是一件好事,此时,可以使用HtmlWebpackPlugin插件提供的htmlWebpackPlugin全局变量。

HtmlWebpackPlugin配置项中传入额外的参数,然后再模板上通过htmlWebpackPlugin.options[key]的形式,就可以访问到预先在配置文件中定义的变量

// webpack.config.js
new HtmlWebpackPlugin({
    // 省略了其他配置
    setCDN(url){
        return "http://localhost:9999/assets/" + url;
    }
}),

// page1.ejs
<img src="<%= htmlWebpackPlugin.options.setCDN('1.png') %>" alt="">

在输出的模板文件上面可以看见<img src="http://localhost:9999/assets/1.png" alt="">。因此,模板通用的变量啥的都可以这样定义然后调用。需要注意的是,即使不是在ejs文件中使用这种语法也是可以的。

2. 开发环境

在目前的开发过程中一般会经历三个开发环境:

  • 本地localhost开发,浏览器热更新
  • 本地服务器配置虚拟域名开发,调试相关路由和数据
  • 线上生产环境

在不同的开发环境下,需要的输出文件可能是不一样的,这意味着需要频繁改动配置文件,这是一个很容易出问题的活(之前这么搞把配置弄炸了两三次)。更好的做法是使用package.jsonscripts,通过不同的命令来执行不同的打包配置。呃没错,接下来我们来搭建一个简陋的webpack开发环境。

2.1. 环境切换

由于可能需要在不同的开发环境来回切换,为了减少对于配置文件的改动,因此使用--env参数并在配置文件中进行判断,然后导出整个配置文件。

// package.json
"scripts": {
  "dev": "webpack --config ./config/webpack.base.js --env.dev",
},

为了更方便的进行设置,将相关的配置都集中在了config.js文件中,然后在多个文件之间使用process.env.NODE_ENV判断判断环境

module.exports = (env)=>{
    /*==========env==========*/
    ["dev", "test", "build"].forEach(item=>{
        if (env[item]){
            process.env.NODE_ENV = item;
        }
    });

    // 其他的配置文件中就可以根据process.env.NODE_ENV来判断对应的环境了
}

2.2. 文件配置

config.js中声明了

  • 需要独立打包的样式表,对应util.style.js文件
  • 模板文件及相关配置,对应util.tpl.js文件,
  • 入口文件,对应webpack.base.js文件
  • 跟环境相关的配置

2.3. 源码

由于代码较多,这里将整个项目整理到github上,这里是webpackTpl传送门,在后续的工作过程中应该会逐步更新。