通过 NPM 创建项目
无论是这个旨在梳理、测试,或者一个正式的、需要通过打包工具来构建的前端项目,通过 npm init
来创建项目,管理项目依赖已经成了标配。
完成后 NPM 会自动在目录中创建 package.json
文件。
Webpack 基础配置
安装 Webpack
接着是 Webpack 安装和基本配置,在终端中输入以下12:
webpack-cli
是 Webapck 的 Command Line Interface,需要在终端中直接通过命令调用 webpack
命令,需要这个 cli 工具。原来 cli 是和 Webpack 打包在一起的,之后被独立了出来,同时还有一个包叫做 webpack-command
也是 Webapck 的命令行工具,简介里说比较轻量、没有包含所有的命令,没有尝试,下文中使用的都是 webpack-cli
。--save
在是让 NPM 将这个依赖写入到 package.json
中,-dev
的意思则是,这不是一个项目依赖,而是一个开发中需要的依赖。
Webpack 在这里是一个构建工具,对前端开发中的模块文件(module)进行打包,打包完毕之后的代码进行上线部署,但是上线之后并不需要 Webpack 本身,相反的例子可以考虑 Express
这个服务端的 Node.js 框架,如果需要在项目中使用 Express
,一般情况下那就是一个项目依赖,而不是开发依赖。
完成安装后, package.json
文件中多出了如下内容,表示 Webapck 安装成功了:
最基本的文件结构
在文件目录中新建我们需要的 src(source)目录来存放我们的 JavaScript 文件:
然后在 app.js 文件中加入一句,方便之后做最简单的测试
Webpack 中最基本的打包
git checkout
f9aa7b8
, add: devDeps
.Webpack 提供了很多选项供我们在开发的时候灵活调用满足自己的需求,最基本的是 entry
,output
, 前者指定了文件的入口,而后者指定 Webpack 在编译了入口文件后的打包文件(bundle)放在哪里,比如入口是 src/
目录下的 app.js
文件,而打包完成的文件,希望可以放到 dist/
目录下,并且命名为 bundle.js
,那我们在终端中就可以运行:
在这里也许会疑惑,为什么不直接使用 webpack src/js/app.js dist/bundle.js
命令:
有时候会看到文章说 npm install webpack --g
全局安装,之后就可以每次都轻松使用 webpack
作为命令,而不需要输入上面那一长串。但是在绝大多数情况下都不推荐全局安装,因为很可能在不同的项目里会需要的不同版本的 Webapck,全局唯一的安装版本很可能会造成很多未知的问题(之后会提到配置 NPM Script,通过那里,执行 webpack 命令也会很方便,也更加符合最佳实践)。
如果不确定是否已经全局安装了什么,可以通过一下命令检查并且删除(参考 《How to list npm user-installed packages?》、《Uninstalling global packages》 ):
通过配置文件进行打包
但我们在实际项目中不这么干,而是通过配置文件(configuration file) 对 Webpack 进行配置。在项目的根目录创建相关配置文件 Webpack.config.js
文件,并且写入基本配置:
这里的设置非常简单,除了对相应模块的导入之外,只告诉了 Webpack 了两件事:
- 去哪里找入口文件? 入口在
src/
下的app.js
文件; - 把打包好的文件放到哪里? 输出的内容请放到
dist/
下的bundle.js
文件中; - 通过什么方式输出打包内容? 开发(
mode: development
),相对也有生产环境(mode: production
)。
这里的入口文件对应 entry
选项,之所以会有一个入口,是因为既然我们已经使用 Webpack 来做模块打包,那么我们项目的结构也是一个一个模块相互分隔独立又相互联系的。
什么是一个入口?
比如说我们在项目的第一页,引入了 a.js
,a.js
中又依赖于 b.js
和 c.js
,之后 b.js
和 c.js
又分别依赖 e.js
、f.js
和 g.js
、h.js
,以此类推的层层依赖,那么我们不管我们在之后依赖的多少文件,项目的入口始终都是 a.js
,它就是项目所有模块的 entry
。
到这里最简单的构建环境就做好了,切换到终端中,保证自己在项目根目录,输入:
完善目录文件
git checkout
3d1e552
, add: webpack.config.js
.根目录中 Webpack 为我们自动生成了 dist
目录,打开其中的 bundle.js
,拖到底部,就有 app.js
中提取出来的内容,大概类似这样,包含之前写 hello world
例子:
JavaScript 打包完成,我们还缺什么让它运行起来?在浏览器环境中,当然是一个 html 文件。新建 index.html 文件,将基本的结构填充进去,并且在 </body>
之前引入之前成功打包的 bundle.js
;在浏览器中打开,就可以看到 alert
:
加入 Vue.js
安装 Vue.js
git checkout
e426a2b
, add: vue.js
.接下来,因为之后搭建的环境里会以 Vue.js 作为例子来配置 Webpack 中关于 JavaScript 和 Loader
的配置,所以之类做一些基本的关于 Vue.js 的准备工作。首先是安装 Vue.js 并且创建 app.js
作为之后的入口,其次更新 HTML 部分。
Vue.js 的独立构建版本 vs 运行时版本
git checkout
dd1f6fa
, add: resolve vue$ with vue.esm.js
.在终端中再次运行 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-loader
或vueify
的时候,*.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-cli
, vue-loader
, 或者单文件组件,我们的报错信息也是提示我们需要使用 standalone 版本,所以,怎么加? 官方文档针对 webpack 的配置也给除了解决方案, 在 配置中添加下面的别名:
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>
,观察终端中是不是自动重新编译了?再回到浏览器中刷新,内容也发生了变化。
<h1>Hello World!</h1>
修改成 <h1>Hello World with file watching!</h1>
, 终端中会提示自动刷新。加入 webpack-dev-server
作为本地开发环境
git checkout
3ff9c18
, add: 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-server
与 webapck
打包位置的区别
这里需要强调,之前通过终端中输入 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
git checkout
80e9987
, add: dev-server options in webpack.config.js
.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
git checkout
2117919
, add: npm scripts
.目前为止无论是编译打包,或者是本地运行测试服务器的命令都很长:
在正常工作会为此做一个优化,为每个命令提供一个 shortcut,怎么做?通过 npm ,再具体一点是通过开始我们创建项目执行了 npm init
生成的 package.json
中的 script
属性,如下:
webpack
命令中加入了一个新的 flag:--hide-modules
,在编译的时候,不需要告诉我们在编译哪些模块,只需要提示是否成功就可以。还有一些相关的如:--color
,--progress
可以配合习惯使用。
在 NPM 与终端中调用命令有一个小区别:npm 中执行命令会自动为我们去 ./node_modules/
文件夹下寻找,于是我们就不需要使用那一长串冗长的文件路径,可以直接这么写:
和上面列出来的效果一致。而且通过这种方式,也可以完全避免上文提到的全局安装 Webpack 的问题。
对应的,在终端中的调用就是:
Vue 的单文件组件和 Vue-Loader
git checkout
ad15007
, update: convert to Vue.js single-file-component
.
添加 vue-loader
,转换成 Vue.js 的 Single-File-Component,并重新进行文件组织。
配合 Vue.js 来配置 Webapck 中,绕不过去的就是 Webpack 的各种 Loader,以及 Vue.js 本身的单文件组件也只能通过 vue-loader 来进行来编译使用。做项目的时候单文件组件的组织方式会使得文件组织干净很多,所以更新这篇笔记的时候,把这一部分加了进来。
转换成 Vue.js 单文件组件
这一部分要做的就是将上文中已经完成的配置转换成 Vue 单文件组织的写法,首先安装需要的依赖。
vue-loader
本身依赖于 css-loader
和 vue-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
git checkout
022c445
, dd: UglifyJsPlugin
.
mode: production
的时候,使用 UglifyJs 对输出的 bundle.js
进行打包。
这一部分的须知:
因为之前写的笔记包含了使用 UglifyJS 进行打包的内容,所以这里对内容做了更新。
但是在 Webpack 4 里,当 mode:production
会作自动的打包并且压缩。
本地测试了一下,把 webpack.config.js
设置成 production
,添加、删除下面 gist 里最后这一段内容,运行 npx webpack
进行打包,输出的 bundle.js
大小一样。查了一下没有明确的答案,所以这一段先保留下来。
生产环境 vs 测试环境
git checkout
8c04200
, update: npm script with process.NODE_EVN
.
添加 npm run dev
、 npm run server
和 npm run production
这些 script
。
泛泛而言,线上部署的环境被成为生产环境,本地测试、或者线上的测试机器被称为测试环境,使用 Uglify.js 进行打包我们制定了是在生产环境的 process.env.NODE_ENV === 'production'
,在本地想要执行测试,方法很简单,在 npm run serve
的时候,告诉终端,我们需要 production
环境:
得说明的是,在这个命令输入到终端里,当前终端的 session 里 NODE_ENV
就一直是 production
的值了,那么我们还想恢复到不需要 uglify.js 压缩的状态的 development 状态要怎么办?可想而知,就是将 development
传给 NODE_ENV
:
所以我们需要对之前定义好的 npm script
做一些修改,如下:
Babel
git checkout
df5e881
, add: Babel
.
为 Webpack 安装 babel-loader
需要的相关依赖,配置。
安装 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。
target
6 指定可以通过 BROWSERSLIST
环境变量、browserslist
配置文件、.browserslistrc
配置文件、或者直接在 package.json
中指定 browserslist
键名等方法,官方推荐使用最后一种在 package.json
中指定键名的方式,笔记中使用直接在 .babelrc
中指定 target
的办法。
本地使用 Vue.js 进行开发,通过 Webpack 实现热重载
热重载(Hot Module Replacement/Hot Reload),Webpack 官方提供的指南(guide)、和概念(concept) 并不是非常直观,大致概括一下在本地开发过程中开启热重载会发生什么:
- 使用 Vue.js 写了一个项目;
- 其中一个页面有一个
state
,通过 Vue.js 进行渲染输出在了页面; - 写完了放到浏览器里去测试,这个
state
随着测试在浏览器中已经发生了改变;这时页面里的<tempalte>
有一些地方需要更新,比如为了样式修改一下结构。
当没有热重载的时候,回到编辑器修改 <template>
、保存,Webpack 监听到文件变化之后,会对浏览器里的测试页面进行完全刷新(full reolad),state
在页面刷新之后会丢失。而当开启热重载,测试页面仍然会刷新,但是 state
的值会得以保留。
在 webpack-dev-server
中开启热重载
git checkout
88ee0af
, update: config for HMR.
添加了 webpack-dev-server
开启 HMR 热重载的配置选项。
在 Webpack 中开启热重载很简单,通过 devServer
中的 hot:true
选项,以及添加 HotModuleReplacementPlugin
这个插件。但同时也有坑,devServer
和 output
两个属性中的 publicPath
需要一致,但是写法却不同。
output
中的publicPath
: 得写成绝对值,并且包含端口号(port
);devServer
中的hotOnly
: 指的是只能进行热重载,如果不成功页面不进行刷新,帮助 debug;plugins
中的NamedModulesPlugin
: 并不是必须、目的是在终端中输出的时候可以看到具体文件名,同样也是为了 debug。
以一个计数器(counter.vue
)作为范例
git checkout
8a0e956
, add: counter.vue as HMR exmaple
.
添加了一个计数器组件(counter.vue)作为 HMR 热重载的例子。
热重载默认是开启的,除非遇到以下情况(官方文档):
- Webpack 的
target
的值是node
(服务端渲染); - Webpack 会压缩代码;
process.env.NODE_ENV === 'production'
。
这里以一个计数器(counter)作为例子:
counter.vue
加入到 App.vue
,请看 diff
。打开浏览器会看到计数器、以及 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 也成功引入
*.hot-update.js
和 *.hot-update.js
完工前:测试、生产环境的不同输出
git checkout
c54e1f1
, update: mode: prod/dev for output
.
这里除了下面贴得代码片段,package.json
里也做了修改。为什么? 因为写错了,npm run dev
的时候没有传入 export NODE_ENV=development
。
上面说到通过 npm script
将 process.env.NODE_ENV
通过终端 export
的方式传入,但只写了一半,Vue.js 通过 Webpack 构建的时候也会输出不同的版本,同样也是通过 env
,最直观区别是当输出完毕在浏览器中打开项目的时候:
mode: developemnt
: 会出现You are running Vue in development mode.
警告,还会有很多info
、warning
;mode: production
: 则什么都没有。
具体的传入很简单,给 mode
做个判断:
之后分别通过 npm run dev
和 npm 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
git checkout
b296876
, finish with a production bundle
.
到上一个提交已经完成,这里最后提交了一个 mode: production
的 bundle.js
。
Finally.
在大陆的小伙伴如果 NPM 的速度感人可以使用马云家提供的镜像服务:链接,按照首页下方「使用说明」中的方法安装完成后,就可以使用
cnpm install [module]
来安装,速度很快,但已知问题是不会主动生成package-lock.json
文件。 ↩︎除了马云家提供的镜像,还可以使用脸书的 Yarn,相对的命令需要从
npm install webpack --save-dev
换成yarn add webpack --dev
。最近新建项目的时候几乎都是使用 Yarn 来拉取依赖,因为速度相对快,速度这个事情不同地区、不同运行商之间都有区别,自己测试适用合适的就好。 ↩︎如果想要在使用
webpack-dev-server
的时候同时将 bundle 写入磁盘,可以打开两个终端窗口,一个运行webpack-dev-server
,一个运行webpack --watch
;或者参考这个插件: write-file-webpack-plugin。 ↩︎关于 Babel 最近本的使用首先可以参阅 Babel 官网,其次是一些基本的入门指南:《Babel 入门教程》、《Babel 使用指南》,《Babel polyfill 知多少》、《深入理解ES6 阅读笔记 — babel》。 ↩︎
官方宣布
@babel/preset-env
的文章《babel-preset-es2015 -> babel-preset-env;@babel/preset-env 的官方 repo;以及据说@babel/preset-env
的出现来自于这条 tweet,以及 tweet 发出后 Addy Osmani 写的一篇草稿。 ↩︎《再见,babel-preset-2015》,上面写在
.babelrc
中指定@babel/preset-env
的target
选项的基本使用,这篇专栏做了展开,如{ modules: false }
,还有useBuiltIns
等。 ↩︎
技术发展迭代很快,所以这些笔记内容也有类似新闻的时效性,不免有过时、或者错误的地方,欢迎指正 ^_^。
BEST
Lien(A.K.A 胡椒)