笔记:配合 Vue.js 配置 Webpack(一)Vue.js,Webpack 和 Webapck Dev Server 的基本配置


 拍摄于 2016-08-21
拍摄于 2016-08-21

通过 NPM 创建项目

无论是这个旨在梳理、测试,或者一个正式的、需要通过打包工具来构建的前端项目,通过 npm init 来创建项目,管理项目依赖已经成了标配。

-y 在这里是表示快速新建。使用 npm init 来初始化,终端会一步一步提示输入 nameversiondescription 等项目 meta 信息,通过使用 flag -y 跳过所有的 prompt,一路到底完成默认值的初始化。

完成后 NPM 会自动在目录中创建 package.json 文件。

package.json 大致内容应该类似。

Webpack 基础配置

安装 Webpack

接着是 Webpack 安装和基本配置,在终端中输入以下12

使用 NPM 或者 Yarn 来管理安装依赖。

完成安装后, package.json 文件中多出了如下内容,表示 Webapck 安装成功了:

最基本的文件结构

在文件目录中新建我们需要的 src(source)目录来存放我们的 JavaScript 文件:

然后在 app.js 文件中加入一句,方便之后做最简单的测试

Webpack 中最基本的打包

Webpack 提供了很多选项供我们在开发的时候灵活调用满足自己的需求,最基本的是 entryoutput, 前者指定了文件的入口,而后者指定 Webpack 在编译了入口文件后的打包文件(bundle)放在哪里,比如入口是 src/ 目录下的 app.js 文件,而打包完成的文件,希望可以放到 dist/ 目录下,并且命名为 bundle.js,那我们在终端中就可以运行:

上下两句等同,./node_modules/.bin/webpack 的意思是在当前目录下指定 webpack 可执行文件的位置、并且执行,npx 则将「寻找可执行文件目录的位置」这件事替我们完成了。

如果在终端中提示 The ‘mode’ option has not been set ,可以通过 npx webpack ./src/js/app.js -o ./dist/bundle.js --mode development 解决,这边没有加因为之后会写到。简单提一下,随着 Webpack 4 推出的「零配置」概念,它也要求在配置中写入一个 mode 的选项,development 或者是 production,通过这个选项 Webpack 会自动输出合适的打包。

通过配置文件进行打包

但我们在实际项目中不这么干,而是通过配置文件(configuration file) 对 Webpack 进行配置。在项目的根目录创建相关配置文件 Webpack.config.js 文件,并且写入基本配置:

这里的设置非常简单,除了对相应模块的导入之外,只告诉了 Webpack 了两件事:

  1. 去哪里找入口文件? 入口在 src/ 下的 app.js 文件;
  2. 把打包好的文件放到哪里? 输出的内容请放到 dist/ 下的 bundle.js 文件中;
  3. 通过什么方式输出打包内容? 开发(mode: development),相对也有生产环境(mode: production)。

这里的入口文件对应 entry 选项,之所以会有一个入口,是因为既然我们已经使用 Webpack 来做模块打包,那么我们项目的结构也是一个一个模块相互分隔独立又相互联系的。

到这里最简单的构建环境就做好了,切换到终端中,保证自己在项目根目录,输入:

完善目录文件

根目录中 Webpack 为我们自动生成了 dist 目录,打开其中的 bundle.js,拖到底部,就有 app.js 中提取出来的内容,大概类似这样,包含之前写 hello world 例子:

JavaScript 打包完成,我们还缺什么让它运行起来?在浏览器环境中,当然是一个 html 文件。新建 index.html 文件,将基本的结构填充进去,并且在 </body> 之前引入之前成功打包的 bundle.js;在浏览器中打开,就可以看到 alert:

加入 Vue.js

安装 Vue.js

接下来,因为之后搭建的环境里会以 Vue.js 作为例子来配置 Webpack 中关于 JavaScript 和 Loader 的配置,所以之类做一些基本的关于 Vue.js 的准备工作。首先是安装 Vue.js 并且创建 app.js 作为之后的入口,其次更新 HTML 部分。

Vue.js 的独立构建版本 vs 运行时版本

在终端中再次运行 npx webpack 命令进行编译,成功后再浏览器中刷新我们之前打开的 index.html什么也么有对不对,说好的 <h1>Hello World</h1> 无处寻觅,知道发生问题了,那么问题在哪儿呢?打开浏览器的 inspector,切换到 console 控制台,会有报错信息如下:

Vue warn: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.

这一段什么意思?官方文档中《运行时 + 编译器 vs. 只包含运行时》部分中有说明,主要区别在于前者:独立构建(standalone)包含模板编译器而后者:运行时版本(runtime)不包含。

当使用 vue-loadervueify 的时候,*.vue 文件内部的模板会在构建时预编译成 JavaScript。你在最终打好的包里实际上是不需要编译器的,所以只用运行时版本即可。

因为运行时版本相比完整版体积要小大约 30%,所以应该尽可能使用这个版本。如果你仍然希望使用完整版,则需要在打包工具里配置一个别名:

意味着独立构建(standalone)允许在申明组件的时候使用 template 选项,如下。也就是说这个构建版本会替开发者搞定从模板编译(template)到运行时渲染(render)的所有,也意味着使用这个构建版本,开发者可以从 CDN 引入 <script> 的方法来进行开发:

相对以上,运行时版本(runtime only build)就好理解了,简而言之就就是 Vue 在这个版本里没有替我们搞定模板编译(template)的环节。那么问题又来了,Vue 不帮我们渲染编译,谁来? vue-cli!通过 webpack + vue-loader 的方式来预编译字符串模板,之后只需要使用运行时版本,更详细的可以参考 vue 官方文档中的《单文件组件(single file component)部分》, 这里不做过多展开。

不过需要明确的是: 这里我希望通过 webpack 来打包 vue, 但是没涉及到 vue-clivue-loader, 或者单文件组件,我们的报错信息也是提示我们需要使用 standalone 版本,所以,怎么加? 官方文档针对 webpack 的配置也给除了解决方案, 在 配置中添加下面的别名:

这句等同于在进行构建打包时, 告诉 Webpack 「我知道你在 vue 的 npm 包里首先找到了 vue.js,但那是运行时版本,我还没到那个步骤,给你一个别名,每次遇到 import vue 的时候, 请你忽略默认 vue.js,自动去找 vue 包里 vue.esm.js

完成后,在终端里运行 npx webpack 命令,打开浏览器,说好的 <h1>Hello World</h1> 如期而至:

如期而至的 <h1>Hello World</h1>

本地自动构建环境搭建

webpack 监听文件变化自动编译

Hello World 成功之后,我们考虑需要一个更加自动化的构建环境。 自动化构建环境指的是: 我们每次编辑好 JavaScript 文件,不需要去终端里输入 webpack,自动构建会监听文件变动,为我们主动重新编译打包。如何做到:我们可以在终端中输入 webpack 命令的时候追加一个 --watch 选项:

回到 app.js 中,将 template 选项属性中的 <h1>Hello World!</h1> 修改成 <h1>Hello World with file watching!</h1>,观察终端中是不是自动重新编译了?再回到浏览器中刷新,内容也发生了变化。

webpack is watching the files…<h1>Hello World!</h1> 修改成 <h1>Hello World with file watching!</h1>, 终端中会提示自动刷新。

监听文件变动,进行重新自动打包的工作完成了。

加入 webpack-dev-server 作为本地开发环境

webpack-dev-server 并不必须,特别是服务器端不是使用 Node.js 的时候,比如 PHP 可以是在本地通过 Apache 的 virtual host 的方式,那样的话只需要 npx webpack --watch 监听文件变化自动打包就可以,在这类加入 webpack-dev-server 是为了方便开发测试预览。

安装,之后直接运行:

--output-public-path /dist 在这里起到的作用稍后会提到。

监听文件变动,进行重新自动打包的工作完成了。

切换到浏览器,打开 http:localhost:8080。回到在 app.js 中再次修改 template<h1>Hello World with webpack-dev-server</h1>,查看终端,会进行自动重新编译打包,切换到浏览器,页面主动刷新内容得到了更新。

查看终端会发现 webpack-dev-server 提示了重新进行了文件打包,同时在浏览器里的内容也会自动刷新。

webpack-dev-serverwebapck 打包位置的区别

这里需要强调,之前通过终端中输入 npx webpack 命令,我们编译打包放在 dist 目录下,输出为 bundle.js

但是在通过 webpack-dev-server 进行本地测试开发的时候,不会重新打包输出到 dist 目录;但同时它确实打包了,但是具体的内容存放在了内存之中,可以通过 index.html<script> 标签引用,意味如果现在打开 dist/ 目录中的 bundle.js,拖到最底下,原来的 <h1>Hello World</h1> 并没有发生变化(下图)

使用 webpack-dev-server 的时候,没有写入将 bundle 写入到 output.path 的原因可以理解为:之所以没有写入 bundle 就是因为它是个 webpack-dev-server,推荐的方式是在本地开发(dev)步骤彻底完成之后,再去通过 webpack 命令来进行打包编译3

通过 webpack.config.js 配置 webpack-dev-server

webpack-dev-server 作为一个本地的测试服务器,也可以通过 webpack.config.js 来进行配置,比如我们在运行的时候传入了一个 flag:--output-public-path

webpack.config.js 中,可以通过 devServer 来添加;还有一些常用的选项,属性名和对应产生的效果挺直关的,如下(比较习惯用 1333 端口进行本地测试,也不知道是为啥)

配置中的 output.publicPath

也就是上文测试 webpack-dev-server 一开始传入的那个 flag,它的作用是:为我们在本地测试环境里提供一个「不存在」但是可以访问的文件(这里的「不存在」指的是存在于内存中)。

比如我们的原始文件都是放在了 src/ 目录下,而通过 webpack-dev-server 编译打包的文件放在了 dist/ 目录下。

但是有时候我们想将编译好的文件放到别的名称目录下输出,比如 www/assets/ 这样,我们就可以在 derServer.publicPath 中填入对应的 www/assets/,然后在 index.html 中就不是引用现在的 dist/bundle.js,而是 www/assets/bundle.js,当我们请求的这个路径的时候,webpack-dev-server 会自动为我们找到我们仍然在 dist/bundle.js 的文件。

添加 npm script

目前为止无论是编译打包,或者是本地运行测试服务器的命令都很长:

在正常工作会为此做一个优化,为每个命令提供一个 shortcut,怎么做?通过 npm ,再具体一点是通过开始我们创建项目执行了 npm init 生成的 package.json 中的 script 属性,如下:

webpack 命令中加入了一个新的 flag:--hide-modules,在编译的时候,不需要告诉我们在编译哪些模块,只需要提示是否成功就可以。还有一些相关的如:--color--progress 可以配合习惯使用。

在 NPM 与终端中调用命令有一个小区别:npm 中执行命令会自动为我们去 ./node_modules/ 文件夹下寻找,于是我们就不需要使用那一长串冗长的文件路径,可以直接这么写:

和上面列出来的效果一致。而且通过这种方式,也可以完全避免上文提到的全局安装 Webpack 的问题。

对应的,在终端中的调用就是:

Vue 的单文件组件和 Vue-Loader

配合 Vue.js 来配置 Webapck 中,绕不过去的就是 Webpack 的各种 Loader,以及 Vue.js 本身的单文件组件也只能通过 vue-loader 来进行来编译使用。做项目的时候单文件组件的组织方式会使得文件组织干净很多,所以更新这篇笔记的时候,把这一部分加了进来。

转换成 Vue.js 单文件组件

这一部分要做的就是将上文中已经完成的配置转换成 Vue 单文件组织的写法,首先安装需要的依赖。

vue-loader 本身依赖于 css-loadervue-template-compiler

重新组织文件,添加 App.vue、修改 app.js 作为入口

webpack.config.js 加入 .vue$ 的支持 vue-loader

Webpack 4 里使用 Vue-Loader 的写法和之前有点区别,除了在 module.rules 里面进行声明,同时还需要在 plugins 里面 push 一下,

三步完成之后,在终端里重新运行 npm run serve,浏览器中的内容应该和上一步一模一样,但此时我们已经把文件组织换成了 Vue.js 的单文件组件。

将项目转成单文件组织之后成功后,运行 npm run serve 后在浏览器中的输出和上一步没有任何区别。

生产环境中使用 UglifyJs 打包 bundle.js

生产环境 vs 测试环境

泛泛而言,线上部署的环境被成为生产环境,本地测试、或者线上的测试机器被称为测试环境,使用 Uglify.js 进行打包我们制定了是在生产环境的 process.env.NODE_ENV === 'production',在本地想要执行测试,方法很简单,在 npm run serve 的时候,告诉终端,我们需要 production 环境:

得说明的是,在这个命令输入到终端里,当前终端的 session 里 NODE_ENV 就一直是 production 的值了,那么我们还想恢复到不需要 uglify.js 压缩的状态的 development 状态要怎么办?可想而知,就是将 development 传给 NODE_ENV

所以我们需要对之前定义好的 npm script 做一些修改,如下:

Babel

安装 Babel

Babel 官方网站在这里,笼统一点说是一个语法转换工具,和 Webpack 不同的点在于,其针对的是 ES6 或者 ES7 的语法,可以妥妥地将新版本的 ES6、ES7 的语法转换成就浏览器可以识别解析的 ES5;而后者则是前端打包工具,配合 Babel 可以转换打包 ES6、ES7,同时配合 sass-loader 也可以转换 .scss 文件,或者 file-loader 来处理文件4

通过 npm 安装以下依赖:

babel-loader、babel-core、@babel/preset-env 三者的作用:

  • babel-core:上面说道,Babel 是一个将 ES 新语法转换成就旧版语法的工具,或者也可以说是为所有的新的语法做了 polyfill,这个 babel-core 在命名上就很直接,它就是所有转换功能的 core5
  • babel-loader:Webpack 在处理不同类型文件的模块打包时,使用 loader 这个概念来对需要打包的文件进行分类处理,比如这里以 .js 结尾的 JavaScript 文件,在归类的时候首先使用正则 /\.js$/ 来匹配文件名的结尾,之后,比如我们需要使用 Babel 进行转换,则使用 babel-loader,而 babel-loader 则会对使用 babel-core 中的 polyfill 对文件进行合适的转换;
  • @babel/preset-env:基于不同运行环境来自动对代码进行转换的工具,这里的环境可以是浏览器也可以是 Nodejs。与 babel-preset-es2015 的区别可以考虑这样的情况:Chrome 肯定好于 ES2015 的支持好于 IE,而 Chrome 的高版本比如 v60 肯定好于低版本比如 v40,那么在不同浏览器,和相同浏览器的不同版本里,需要 babel 进行的代码转换的量应该是不同的,所以一股脑将所有的代码都转换显得不明智,何不将需要转换的量通过一个变量自动传给 babel,然后 babel 再根据具体的需求自动经行转换。这里的自动传给 babel 的变量就是 @babel/preset-env

其次,在 webpack.config.js 中为 .js 结尾的文件使用 babel-loader 进行转换的配置;同时,也需要告诉 babel 如何对代码进行转换。

babel-preset-env 的使用到这里并没有完成,环境变量 env 需要明确指定环境是什么,这里的环境可以是 Nodejs 的版本,也可以是具体需要适配的浏览器的版本,但需要明确指定。如何指定?可以使用 target 键,target 具体语法则需要使用 browserslist 的 query

target6 指定可以通过 BROWSERSLIST 环境变量、browserslist 配置文件、.browserslistrc 配置文件、或者直接在 package.json 中指定 browserslist 键名等方法,官方推荐使用最后一种在 package.json 中指定键名的方式,笔记中使用直接在 .babelrc 中指定 target 的办法

本地使用 Vue.js 进行开发,通过 Webpack 实现热重载

热重载(Hot Module Replacement/Hot Reload),Webpack 官方提供的指南(guide)、和概念(concept) 并不是非常直观,大致概括一下在本地开发过程中开启热重载会发生什么:

  1. 使用 Vue.js 写了一个项目;
  2. 其中一个页面有一个 state,通过 Vue.js 进行渲染输出在了页面;
  3. 写完了放到浏览器里去测试,这个 state 随着测试在浏览器中已经发生了改变;这时页面里的 <tempalte> 有一些地方需要更新,比如为了样式修改一下结构。

当没有热重载的时候,回到编辑器修改 <template>、保存,Webpack 监听到文件变化之后,会对浏览器里的测试页面进行完全刷新(full reolad),state 在页面刷新之后会丢失。而当开启热重载,测试页面仍然会刷新,但是 state 的值会得以保留。

webpack-dev-server 中开启热重载

在 Webpack 中开启热重载很简单,通过 devServer 中的 hot:true 选项,以及添加 HotModuleReplacementPlugin 这个插件。但同时也有坑,devServeroutput 两个属性中的 publicPath 需要一致,但是写法却不同

  1. output 中的 publicPath: 得写成绝对值,并且包含端口号(port);
  2. devServer 中的 hotOnly: 指的是只能进行热重载,如果不成功页面不进行刷新,帮助 debug;
  3. plugins 中的NamedModulesPlugin : 并不是必须、目的是在终端中输出的时候可以看到具体文件名,同样也是为了 debug。

以一个计数器(counter.vue)作为范例

这里以一个计数器(counter)作为例子:

打开浏览器会看到计数器、以及 console 中显示 [WDS] Hot Module Replacement enabled

计数器 counter.vue

console 中显示 [WDS] Hot Module Replacement enabled.

随便把计数器按到多少(我按到了 11)。

随便把计数器按到多少。

回到编辑器,在 <template> 做些修改(我把描述文字删了)

回到浏览器,会发现,我删除的描述文字没有了,但同时之前的计数器为 11 值 state,被保留了下来。

计数器中的 state 被保留了下来。

同时 console 中也给出了提示 [WDS] App hot update...

[WDS] App hot update...

观察终端中输出,配合之前加入的 NamedModulesPlugin,可以看到一串 *.hot-update.js*.hot-update.js 成功生成。Webpack 也成功引入

Webpack 成功引入生成的 *.hot-update.js*.hot-update.js

完工前:测试、生产环境的不同输出

上面说到通过 npm scriptprocess.env.NODE_ENV 通过终端 export 的方式传入,但只写了一半,Vue.js 通过 Webpack 构建的时候也会输出不同的版本,同样也是通过 env,最直观区别是当输出完毕在浏览器中打开项目的时候:

  1. mode: developemnt: 会出现 You are running Vue in development mode. 警告,还会有很多 infowarning
  2. mode: production: 则什么都没有。

具体的传入很简单,给 mode 做个判断:

之后分别通过 npm run devnpm run production 来做不通的输出(可以观察 dist 目录下的 bundle.js 的内容,生产环境的输出会压缩,而开发环境则没有 )。

npm run dev, 即 mode: development,输出之后浏览器的 console 的会有 You are running Vue in development mode. 等内容。

npm run production, 即 mode: production,输出后浏览器的 console 里什么都不会有。

完成配置后的 webpack.config.js

Finally.


  1. 在大陆的小伙伴如果 NPM 的速度感人可以使用马云家提供的镜像服务:链接,按照首页下方「使用说明」中的方法安装完成后,就可以使用 cnpm install [module] 来安装,速度很快,但已知问题是不会主动生成 package-lock.json 文件。
  2. 除了马云家提供的镜像,还可以使用脸书的 Yarn,相对的命令需要从 npm install webpack --save-dev 换成 yarn add webpack --dev。最近新建项目的时候几乎都是使用 Yarn 来拉取依赖,因为速度相对快,速度这个事情不同地区、不同运行商之间都有区别,自己测试适用合适的就好。
  3. 如果想要在使用 webpack-dev-server 的时候同时将 bundle 写入磁盘,可以打开两个终端窗口,一个运行 webpack-dev-server,一个运行 webpack --watch;或者参考这个插件: write-file-webpack-plugin
  4. 关于 Babel 最近本的使用首先可以参阅 Babel 官网,其次是一些基本的入门指南:《Babel 入门教程》、《Babel 使用指南》,《Babel polyfill 知多少》、《深入理解ES6 阅读笔记 — babel》。
  5. 官方宣布 @babel/preset-env 的文章《babel-preset-es2015 -> babel-preset-env@babel/preset-env 的官方 repo;以及据说 @babel/preset-env 的出现来自于这条 tweet,以及 tweet 发出后 Addy Osmani 写的一篇草稿
  6. 再见,babel-preset-2015》,上面写在 .babelrc 中指定 @babel/preset-envtarget 选项的基本使用,这篇专栏做了展开,如 { modules: false },还有 useBuiltIns 等。

感谢阅读

你们好, 2018 年初把小站从 Jekyll 迁移到 Hugo 的过程中,删除了评论区放的 Disqus 插件,考虑有二:首先无论评论、还是对笔记内容的进一步讨论,读者们更喜欢通过邮件、或者 Twitter 私信的方式来沟通;其次一年多以来 Disqus 后台能看到几乎都是垃圾留言(spam),所以这里直接贴一下邮件、以及 Twitter 账户 地址。

技术发展迭代很快,所以这些笔记内容也有类似新闻的时效性,不免有过时、或者错误的地方,欢迎指正 ^_^。

BEST
Lien(A.K.A 胡椒)
本站总访问量 本站总访客量 本文总阅读量