上手先搞一个简单的SPA应用

一上来步子太大容易扯到蛋, 让我们先弄个最简单的webpack配置来热一下身.

安装Node.js

webpack是基于我大Node.js的打包工具, 上来第一件事自然是先安装Node.js了, 传送门->.

初始化一个项目

我们先随便找个地方, 建一个文件夹叫simple, 然后在这里面搭项目. 完成品在examples/simple目录, 大家搞的时候可以参照一下. 我们先看一下目录结构:

  1. ├── dist 打包输出目录, 只需部署这个目录到生产环境
  2. ├── package.json 项目配置信息
  3. ├── node_modules npm安装的依赖包都在这里面
  4. ├── src 我们的源代码
  5. ├── components 可以复用的模块放在这里面
  6. ├── index.html 入口html
  7. ├── index.js 入口js
  8. ├── libs 不在npmgit上的库扔这里
  9. └── views 页面放这里
  10. └── webpack.config.js webpack配置文件

打开命令行窗口, cd到刚才建的simple目录. 然后执行这个命令初始化项目:

  1. npm init

命令行会要你输入一些配置信息, 我们这里一路按回车下去, 生成一个默认的项目配置文件package.json.

给项目加上语法报错和代码规范检查

我们安装eslint, 用来检查语法报错, 当我们书写js时, 有错误的地方会出现提示.

  1. npm install eslint eslint-config-enough eslint-loader --save-dev

npm install可以一条命令同时安装多个包, 包之间用空格分隔. 包会被安装进node_modules目录中.

--save-dev会把安装的包和版本号记录到package.json中的devDependencies对象中, 还有一个--save, 会记录到dependencies对象中, 它们的区别, 我们可以先简单的理解为打包工具和测试工具用到的包使用--save-dev存到devDependencies, 比如eslint, webpack. 浏览器中执行的js用到的包存到dependencies, 比如jQuery等. 那么它们用来干嘛的?

因为有些npm包安装是需要编译的, 那么导致windows/mac/linux上编译出的可执行文件是不同的, 也就是无法通用, 因此我们在提交代码到git上去的时候, 一般都会在.gitignore里指定忽略node_modules目录和里面的文件, 这样其他人从git上拉下来的项目是没有node_modules目录的, 这时我们需要运行

  1. npm install

它会读取package.json中的devDependenciesdependencies字段, 把记录的包的相应版本下载下来.

这里eslint-config-enough是配置文件, 它规定了代码规范, 要使它生效, 我们要在package.json中添加内容:

  1. {
  2. "eslintConfig": {
  3. "extends": "enough",
  4. "env": {
  5. "browser": true,
  6. "node": true
  7. }
  8. }
  9. }

业界最有名的语法规范是airbnb出品的, 但它规定的太死板了, 比如不允许使用for-offor-in等. 感兴趣的同学可以参照这里安装使用.

eslint-loader用于在webpack编译的时候检查代码, 如果有错误, webpack会报错.

项目里安装了eslint还没用, 我们的IDE和编辑器也得要装eslint插件支持它.

Visual Studio Code需要安装ESLint扩展

atom需要安装linterlinter-eslint这两个插件, 装好后重启生效.

WebStorm需要在设置中打开eslint开关:

WebStorm ESLint Config

写几个页面

我们写一个最简单的SPA应用来介绍SPA应用的内部工作原理. 首先, 建立src/index.html文件, 内容如下:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. </head>
  6. <body>
  7. </body>
  8. </html>

它是一个空白页面, 注意这里我们不需要自己写<script src="index.js"></script>, 因为打包后的文件名和路径可能会变, 所以我们用webpack插件帮我们自动加上.

然后重点是src/index.js:

  1. // 引入作为全局对象储存空间的global.js, js文件可以省略后缀
  2. import g from './global'
  3. // 引入页面文件
  4. import foo from './views/foo'
  5. import bar from './views/bar'
  6. const routes = {
  7. '/foo': foo,
  8. '/bar': bar
  9. }
  10. // Router类, 用来控制页面根据当前URL切换
  11. class Router {
  12. start() {
  13. // 点击浏览器后退/前进按钮时会触发window.onpopstate事件, 我们在这时切换到相应页面
  14. // https://developer.mozilla.org/en-US/docs/Web/Events/popstate
  15. window.addEventListener('popstate', () => {
  16. this.load(location.pathname)
  17. })
  18. // 打开页面时加载当前页面
  19. this.load(location.pathname)
  20. }
  21. // 前往path, 会变更地址栏URL, 并加载相应页面
  22. go(path) {
  23. // 变更地址栏URL
  24. history.pushState({}, '', path)
  25. // 加载页面
  26. this.load(path)
  27. }
  28. // 加载path路径的页面
  29. load(path) {
  30. // 创建页面实例
  31. const view = new routes[path]()
  32. // 调用页面方法, 把页面加载到document.body中
  33. view.mount(document.body)
  34. }
  35. }
  36. // new一个路由对象, 赋值为g.router, 这样我们在其他js文件中可以引用到
  37. g.router = new Router()
  38. // 启动
  39. g.router.start()

现在我们还没有讲webpack配置所以页面还无法访问, 我们先从理论上讲解一下, 等会弄好webpack配置后再实际看页面效果. 当我们访问 http://localhost:8100/foo 的时候, 路由会加载 ./views/foo/index.js文件, 我们来看看这个文件:

  1. // 引入全局对象
  2. import g from '../../global'
  3. // 引入html模板, 会被作为字符串引入
  4. import template from './index.html'
  5. // 引入css, 会生成<style>块插入到<head>头中
  6. import './style.css'
  7. // 导出类
  8. export default class {
  9. mount(container) {
  10. document.title = 'foo'
  11. container.innerHTML = template
  12. container.querySelector('.foo__gobar').addEventListener('click', () => {
  13. // 调用router.go方法加载 /bar 页面
  14. g.router.go('/bar')
  15. })
  16. }
  17. }

借助webpack插件, 我们可以import html, css等其他格式的文件, 文本类的文件会被储存为变量打包进js文件, 其他二进制类的文件, 比如图片, 可以自己配置, 小图片作为Data URI打包进js文件, 大文件打包为单独文件, 我们稍后再讲这块.

其他的src目录下的文件大家自己浏览, 拷贝一份到自己的工作目录, 等会打包时会用到.

页面代码这样就差不多搞定了, 接下来我们进入webpack的安装和配置阶段.

安装webpack和Babel

我们把webpack和它的插件安装到项目:

  1. npm install webpack webpack-dev-server html-webpack-plugin html-loader css-loader style-loader file-loader url-loader --save-dev

webpack-dev-server是webpack提供的用来开发调试的服务器, 让你可以用 http://127.0.0.1:8080/ 这样的url打开页面来调试, 有了它就不用配置nginx了, 方便很多.

html-webpack-plugin, html-loader, css-loader, style-loader等看名字就知道是打包html文件, css文件的插件, 大家在这里可能会有疑问, html-webpack-pluginhtml-loader有什么区别, css-loaderstyle-loader有什么区别, 我们等会看配置文件的时候再讲.

file-loaderurl-loader是打包二进制文件的插件, 具体也在配置文件章节讲解.

接下来, 为了能让不支持ES6的浏览器(比如IE)也能照常运行, 我们需要安装babel, 它会把我们写的ES6源代码转化成ES5, 这样我们源代码写ES6, 打包时生成ES5.

  1. npm install babel-core babel-preset-env babel-loader --save-dev

这里babel-core顾名思义是babel的核心编译器. babel-preset-env是一个配置文件, 我们可以使用这个配置文件转换ES2015/ES2016/ES2017到ES5, 是的, 不只ES6哦. babel还有其他配置文件. 如果只想用ES6, 可以安装babel-preset-es2015:

  1. npm install babel-preset-es2015 --save-dev

但是光安装了babel-preset-env, 在打包时是不会生效的, 需要在package.json加入babel配置:

  1. {
  2. "babel": {
  3. "presets": [
  4. "env"
  5. ]
  6. }
  7. }

打包时babel会读取package.jsonbabel字段的内容, 然后执行相应的转换.

如果使用babel-preset-es2015, 这里相应的也要修改为:

  1. {
  2. "babel": {
  3. "presets": [
  4. "es2015"
  5. ]
  6. }
  7. }

babel-loader是webpack的插件, 我们下面章节再说.

配置webpack

包都装好了, 接下来, 总算可以进入正题了, 是不是有点心累…呵呵. 我们来创建webpack配置文件webpack.config.js, 注意这个文件是在node.js中运行的, 因此不支持ES6的import语法. 我们来看文件内容:

  1. const { resolve } = require('path')
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. module.exports = {
  4. // 配置页面入口js文件
  5. entry: './src/index.js',
  6. // 配置打包输出相关
  7. output: {
  8. // 打包输出目录
  9. path: resolve(__dirname, 'dist'),
  10. // 入口js的打包输出文件名
  11. filename: 'index.js'
  12. },
  13. module: {
  14. /*
  15. 配置各种类型文件的加载器, 称之为loader
  16. webpack当遇到import ... 时, 会调用这里配置的loader对引用的文件进行编译
  17. */
  18. rules: [
  19. {
  20. /*
  21. 使用babel编译ES6/ES7/ES8为ES5代码
  22. 使用正则表达式匹配后缀名为.js的文件
  23. */
  24. test: /\.js$/,
  25. // 排除node_modules目录下的文件, npm安装的包不需要编译
  26. exclude: /node_modules/,
  27. /*
  28. use指定该文件的loader, 值可以是字符串或者数组.
  29. 这里先使用eslint-loader处理, 返回的结果交给babel-loader处理. loader的处理顺序是从最后一个到第一个.
  30. eslint-loader用来检查代码, 如果有错误, 编译的时候会报错.
  31. babel-loader用来编译js文件.
  32. */
  33. use: ['babel-loader', 'eslint-loader']
  34. },
  35. {
  36. // 匹配.html文件
  37. test: /\.html$/,
  38. /*
  39. 使用html-loader, 将html内容存为js字符串, 比如当遇到
  40. import htmlString from './template.html'
  41. template.html的文件内容会被转成一个js字符串, 合并到js文件里.
  42. */
  43. use: 'html-loader'
  44. },
  45. {
  46. // 匹配.css文件
  47. test: /\.css$/,
  48. /*
  49. 先使用css-loader处理, 返回的结果交给style-loader处理.
  50. css-loader将css内容存为js字符串, 并且会把background, @font-face等引用的图片,
  51. 字体文件交给指定的loader打包, 类似上面的html-loader, 用什么loader同样在loaders对象中定义, 等会下面就会看到.
  52. */
  53. use: ['style-loader', 'css-loader']
  54. },
  55. {
  56. /*
  57. 匹配各种格式的图片和字体文件
  58. 上面html-loader会把html中<img>标签的图片解析出来, 文件名匹配到这里的test的正则表达式,
  59. css-loader引用的图片和字体同样会匹配到这里的test条件
  60. */
  61. test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,
  62. /*
  63. 使用url-loader, 它接受一个limit参数, 单位为字节(byte)
  64. 当文件体积小于limit时, url-loader把文件转为Data URI的格式内联到引用的地方
  65. 当文件大于limit时, url-loader会调用file-loader, 把文件储存到输出目录, 并把引用的文件路径改写成输出后的路径
  66. 比如 views/foo/index.html中
  67. <img src="smallpic.png">
  68. 会被编译成
  69. <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAA...">
  70. <img src="largepic.png">
  71. 会被编译成
  72. <img src="/f78661bef717cf2cc2c2e5158f196384.png">
  73. */
  74. use: [
  75. {
  76. loader: 'url-loader',
  77. options: {
  78. limit: 10000
  79. }
  80. }
  81. ]
  82. }
  83. ]
  84. },
  85. /*
  86. 配置webpack插件
  87. plugin和loader的区别是, loader是在import时根据不同的文件名, 匹配不同的loader对这个文件做处理,
  88. 而plugin, 关注的不是文件的格式, 而是在编译的各个阶段, 会触发不同的事件, 让你可以干预每个编译阶段.
  89. */
  90. plugins: [
  91. /*
  92. html-webpack-plugin用来打包入口html文件
  93. entry配置的入口是js文件, webpack以js文件为入口, 遇到import, 用配置的loader加载引入文件
  94. 但作为浏览器打开的入口html, 是引用入口js的文件, 它在整个编译过程的外面,
  95. 所以, 我们需要html-webpack-plugin来打包作为入口的html文件
  96. */
  97. new HtmlWebpackPlugin({
  98. /*
  99. template参数指定入口html文件路径, 插件会把这个文件交给webpack去编译,
  100. webpack按照正常流程, 找到loaders中test条件匹配的loader来编译, 那么这里html-loader就是匹配的loader
  101. html-loader编译后产生的字符串, 会由html-webpack-plugin储存为html文件到输出目录, 默认文件名为index.html
  102. 可以通过filename参数指定输出的文件名
  103. html-webpack-plugin也可以不指定template参数, 它会使用默认的html模板.
  104. */
  105. template: './src/index.html'
  106. })
  107. ],
  108. /*
  109. 配置开发时用的服务器, 让你可以用 http://127.0.0.1:8080/ 这样的url打开页面来调试
  110. 并且带有热更新的功能, 打代码时保存一下文件, 浏览器会自动刷新. 比nginx方便很多
  111. 如果是修改css, 甚至不需要刷新页面, 直接生效. 这让像弹框这种需要点击交互后才会出来的东西调试起来方便很多.
  112. */
  113. devServer: {
  114. // 配置监听端口, 因为8080很常用, 为了避免和其他程序冲突, 我们配个其他的端口号
  115. port: 8100,
  116. /*
  117. historyApiFallback用来配置页面的重定向
  118. SPA的入口是一个统一的html文件, 比如
  119. http://localhost:8010/foo
  120. 我们要返回给它
  121. http://localhost:8010/index.html
  122. 这个文件
  123. 配置为true, 当访问的文件不存在时, 返回根目录下的index.html文件
  124. */
  125. historyApiFallback: true
  126. }
  127. }

走一个

配置OK了, 接下来我们就运行一下吧. 我们先试一下开发环境用的webpack-dev-server:

  1. ./node_modules/.bin/webpack-dev-server -d --hot

上面的命令适用于Mac/Linux等*nix系统, 也适用于Windows上的PowerShell和bash/zsh环境(Bash on Wbuntu on Windows, Git Bash, Babun, MSYS2等).

如果使用Windows的cmd.exe, 请执行:

  1. node_modules\.bin\webpack-dev-server -d --hot

我在这里安利Windows同学使用Bash on Ubuntu on Windows, 可以避免很多跨平台的问题, 比如设置环境变量.

npm会把包的可执行文件安装到./node_modules/.bin/目录下, 所以我们要在这个目录下执行命令.

-d参数是开发环境(Development)的意思, 它会在我们的配置文件中插入调试相关的选项, 比如打开debug, 打开sourceMap, 代码中插入源文件路径注释.

--hot开启热更新功能, 参数会帮我们往配置里添加HotModuleReplacementPlugin插件, 虽然可以在配置里自己写, 但有点麻烦, 用命令行参数方便很多.

命令执行后, 控制台的最后一行应该是

  1. webpack: bundle is now VALID.

这就代表编译成功了, 我们可以在浏览器打开 http://localhost:8100/foo 看看效果. 如果有报错, 那可能是什么地方没弄对? 请自己仔细检查一下~

我们可以随意更改一下src目录下的源代码, 保存后, 浏览器里的页面应该很快会有相应变化.

要退出编译, 按ctrl+c.

开发环境编译试过之后, 我们试试看编译生产环境的代码, 命令是:

  1. ./node_modules/.bin/webpack -p

-p参数会开启生产环境模式, 这个模式下webpack会将代码做压缩等优化.

大家可能会发现, 执行脚本的命令有点麻烦. 因此, 我们可以利用npm的特性, 把命令写在package.json中:

  1. {
  2. "scripts": {
  3. "dev": "webpack-dev-server -d --hot --env.dev",
  4. "build": "webpack -p"
  5. }
  6. }

package.json中的scripts对象, 可以用来写一些脚本命令, 命令不需要前缀目录./node_modules/.bin/, npm会自动寻找该目录下的命令. 我们可以执行:

  1. npm run dev

来启动开发环境.

执行

  1. npm run build

来打包生产环境的代码.