从零搭建一个 webpack 脚手架工具(一)
- 2019 年 12 月 5 日
- 筆記
webpack 是一个现代 JavaScript 应用程序的静态模块打包器,已经成为前端开发不可获取的工具。特别是在开发大型项目时,项目太大,文件过多导致难以维护,或者是优化网络请求时,webpack 都是不可获取的利器。但是 webpack 配置并没有那么容易,webpack 配置项繁多,繁多的背后是配置的灵活性。许多的框架都是由 webpack 搭建而成,因此学会使用 webpack 可以让自己更好的理解脚手架搭建过程,甚至自己写一个灵活高效的脚手架工具。
从最基础的开始,使用的 webpack 版本是^4.39.2
,搭建时会用到以下技术:
- 从单页面到多页面
- 代码切片
- 热更新
- 热替换
- CSS 分离
- HTML 模板
- babel 的使用
- 支持 img、sass、jsx、css、typescript 等工具
- webpack 一些插件的使用
- postcss 的简单配置
- 不同开发环境的配置
- 配置 react 开发环境
首先,配置 webpack,大致的骨架是这样的,这是最基本的配置内容:
{ entry: "", // 入口配置 mode: 'development', // 环境配置 output: {}, // 打包输出配置 module: {}, // loader 配置项 plugins: [], // 插件配置项 devServer: {}, // 服务器配置 }
那么就开始一一进行配置。
安装 webpack,设置程序打包命令
首先是安装:yarn add webpack --dev
或者 npm install webpack --save-dev
或者 yarn add webpack -D
npm i webpack -D
在开发环境安装。 安装完时候,来到 package.json 文件,在 scripts
项中写入一下命令:
"build": "webpack"
当运行命令 npm run build
后,webpack 默认会在项目根目录下查找一个叫 webpack.config.js
的文件,然后进行打包。当文件名不想叫这个或者不想在根目录下创建这个文件时,可以在后面的 --config
字段之后写上文件的所在路径。例如:
"build" "webpack --config config/webpack.config.dev.js"
这样,当运行时,webpack 就从 项目根路径/config/webpack.config.dev.js
这个路径查找配置文件。
在运行命令时,可能会提醒安装
webpack-cli
输入yes
即可。
entry 入口配置(必须的)
entry 大致有四种写法,分别是字符串的形式、数组形式、函数形式和对象形式。代表的含义分别是:
形式 |
含义 |
举例 |
---|---|---|
字符串的形式 |
这种表示单个入口 |
例如:entry: "path(__dirname,"../src/index.js")" |
数组形式 |
这种也是表示单个入口 |
例如:entry: "["./01.js","./index.js"]" |
函数形式 |
可以是单入口也可以是多入口 |
该函数应该返回一个字符路径、数组或对象作为打包入口 |
对象形式 |
这种表示多个入口 |
例如:entry: {app: './src/app.js',vendors: './src/vendors.js'} |
注意!:第一种和第二种都表示单入口,但含义不同。使用数组的作用是将多个资源预先 合并,在打包的时候, webpack 会将数组的最后一项作为实际的入口路径。
modules.exports = { entry: ["babel-polyfill","./src/index.js"] }
就相当于:
// webpack.config.js modules.exports = { entry: "./src/index.js" } // 在打包后的文件中,会包含数组中所有的路径对应的文件 import "babel-polyfill"; // 以及 ./src/index.js 文件
output 配置
output 配置项很多,有两个是必须的:
path
指定文件输出时的文件夹(不存在时会自动创建);filename
指定文件输出时文件的名字;
单页面
{ entry: path.join(__dirname,"../src/index.js"), // 入口配置 output: { path: path.join(__dirname,"../build"), filename: "index.js" }, // 打包输出配置 }
运行后,就会在 build 文件夹下创建一个 index.js 的打包文件。
多页面
{ entry: { index: path.join(__dirname,"../src/index.js"), demo: path.join(__dirname,'../src/demo.js') }, // 入口配置 output: { path: path.join(__dirname,"../build"), filename: "[name].js" }, // 打包输出配置 }
运行后,在 build 文件夹下就会多出两个文件。注意 output 中 filename 的文件名 —— [name]
这个 name 对应的就是 entry
对象的键。当然,也可以为 filename 指定别的字段,但也是要用 []
包裹。可以指定的字段有:
[name]
当前 chunk 的名字[hash]
此次打包所有资源生成的 hash[id]
指代当前 chunk 的 id[chunkhash]
指代当前 chunk 内容的 hash[query]
模块的 query,例如,文件名 ? 后面的字符串
表中
hash
和chunkhash
的作用:当下一次请求时,请求到的资源会被立刻下载新的版本,而不会用本地缓存。query
也有类似的效果,只是需要人为指定。
publicPath
publicPath
是 output 中的一个配置项,不是必须的。但是它是一个非常重要的配置项。 与 path
属性不同,publicPath
用来指定资源的请求位置,而 path
是用来指定资源的输出位置(打包后文件的所在路径)。在 HTML 页面中,我们可能会通过 <script>
标签来加载 JS 代码,标签中的 src
路径就是一个请求路径(不光是 HTML 中的 JS 文件,也可能是 CSS 中的图片、字体等资源、HTML 中的图片、CSS 文件等)。publicPath 的作用就是指定这部分间接资源的请求位置。 publicPath
有三种形式,分别是:
- 与 HTML 相关,在请求这些资源时会以当前页面 HTML 所在路径加上相对路径,构成实际请求的 URL。
- Host 相关,若 publicPath 的值以 ‘/’开始,则代表此时
publicPath
是以当前页面的 hostname 为基础路径的。 - CDN 相关,上面两种都是相对路径,而这个是 绝对路径。如:publicPath 为:"https://www.example.com/",代表当前路径是 CDN 相关。 举个例子,当使用第一种形式时,当我们使用
html-webpack-plugin
插件动态生成一个 HTML,并打包到 build 文件夹后,JS 文件(指定的 entry)会自动插入到 HTML 中。当我们指定publicPath: '/'
,后就会变成:
当没有指定 publicPath 时,默认是 ""
,即:
而如果是 "/static" 时,HTML 引入的资源路径前都将有一个 "/static"。这个路径是相对于项目根路径的。
html-webpack-plugin 插件
这是一个很实用的插件,在上面的例子中,都没有提到 html,而这个插件可以动态生成 html。下载: npm install html-webpack-plugin -D
或者 yarn add npm install html-webpack-plugin -D
。配置:
const HtmlWebpackPlugin = require('html-webpack-plugin'); { plugins: [ new HtmlWebpackPlugin({ // 以下都是可选的配置项: title: "hello world!", // html 的 title 标签内容 // html 模板路径 template: path.join(__dirname,'../public/index.html'), // title favicon 路径 favicon: path.join(__dirname,'../public/favicon.ico'), // 指定引入的 js 代码 插入到哪里(默认是body最底部,即:true) // 也可以指定字符串:"body" 或 "head" inject: false, // 指定 打包输出后,文件的名字(不指定的话还是原来的名字) filename: "hello.html", // 还有许多配置,这是常用的几个 }) ] }
多个 HTML 页面的配置
有时候,想要配置多页面应用,这时就要多实例化几个这个插件。这时就可能会用到别的一个配置属性 —— chunks
{ entry: { index: path.join(__dirname,"../src/index.js"), demo: path.join(__dirname,'../src/demo.js') }, // 入口配置 output: { path: path.join(__dirname,"../build"), filename: "[name].js" }, // 打包输出配置 plugins: [ new HtmlWebpackPlugin({ chunks: ['index'], // 指定 chunk 的名字(一般就是 entry 对象的键) // html 模板路径 template: path.join(__dirname,'../public/index.html'), favicon: path.join(__dirname,'../public/favicon.ico'), inject: "body", filename: "index.html", }), new HtmlWebpackPlugin({ chunks: ['demo'], // html 模板路径 template: path.join(__dirname, '../public/demo.html'), favicon: path.join(__dirname, '../public/favicon.ico'), inject: "body", filename: "demo.html", }), ], // 插件配置项 }
mode 环境变量
mode 的选项一般是这两者其一:development
或 production
,即:开发模式或生产模式。生产模式的代码一般是压缩过的。 单纯的指定 mode 值,可能不能满足我们的需要,这时可以使用另一种办法来设置 mode 值。就是在 package.json 文件的 'scripts' 命令中传入参数。
// .... "scripts": { "build": "cross-env NODE_ENV='development' webpack --config config/webpack.config.dev.js" } // ....
可以注意到,在之前的命令中,我们在前面又添加了一部分内容:cross-env NODE_ENV='development
这是给 Node.js 的全局变量 process
的 env 属性传入了一个值,前面 cross-env
是一个 npm 包,主要为了解决在 Windows 系统下不支持传值命令。 这样,在 webpack 配置文件中,就可以接收到这个值:
var mode = process.env.NODE_ENV; // development
这样就可以根据传入的值,来对配置文件作进一步的改进:
const mode = process.env.NODE_ENV; const isDev = mode === 'development'; const config = { // 公共配置项,比如 loader、mode、entry 和 output 中相同的配置项 mode: mode, entry: "xxx", // 等等 } if(isDve){ // 是开发模式时的配置,比如: config.devServer = { // 对 webpack-dev-server 的配置 } }else{ // 是生产模式时的配置 } // 最后导出: module.exports = config;
module 配置
这一部分比较多,主要是配置各种 loader,比如 css-loader
,babel-loader
,sass-loader
等等。而这些配置存在于 module.rules 这个配置项中。所有的 loader 都是需要安装的。通过 npm install xxx-loader
或 yarn add xxx-loader
的形式进行安装。
style-loader
和 css-loader
两者有很大不同,css-loader
的作用仅仅是处理 CSS 的各种加载语法,例如 @import
和 url()
等。而 style-loader
才是真正让样式起作用的 loader。因此这两个一般配合使用:
{ module: { rules: [ { test: /.css$/, use: ['style-loader','css-loader'] } ] } }
还需要注意的是:webpack 打包时是按数组从后往前的顺序将资源交给 loader 处理的,因此要把最后生效的放在前面。
loader options
有时候使用一个 loader 时,可能要对它进行一些配置,例如 babel-loader
babel 的一些配置就可以写在 options 里,当然也可以建一个 .babelrc
文件进行配置。当一个 loader 需要配置时,它就不能在 use 属性里是个单纯的字符串了,而是一个对象。
{ module: { rules: [ { test: /.css$/, use: ['style-loader',{ // 对 css-loader 配置时,是个对象 loader: 'css-loader', options: { // css-loader 的配置项 } }] } ] } }
需要注意的是,css 中还不能书写背景图片路径(例如:
background: url()
)。不然会报错。因为加载的不是样式,而是图片,在 webpack 中,想要加载图片,还需要使用file-loader
,之后会介绍。
sass-loader
和 less-loader
sass
和 less
是 CSS 的预处理器,需要安装。而且最终会编译成 CSS,因此我们还需要 style-loader
和 css-loader
。而且还要安装编译 Sass 的包:node-sass
(不然会报错)。
module: { rules: [ { test: /.(sass|scss)$/, use: ["style-loader", "css-loader","sass-loader"] }, { test: /.less$/, use: ["style-loader", "css-loader", "less-loader"] } ] }
html-loader
有了这个 loader,我们可以将一个 html 文件通过 JS 加载进来。比如这样:
rules: [ { test: /.html$/, use: 'html-loader' } ] // loader.html <h1>Hello World!</h1> // index.js import html from './loader.html'; document.write(html);
file-loader
和 url-loader
file-loader
用于打包文件类型的资源。比如 CSS 的背景图片和字体、HTML 的 img 标签中的 src 路径等。
rules: [ { test: /.(png|jpg|gif)$/, use: "file-loader", } ]
这样就可以对 png、jpg、gif 类型的图片文件进行打包,而且可以在 JS 中加载图片。
file-loader 中的 options
主要有两个配置项:
name
,指定打包后文件的名字,默认是 hash 值加上文件后缀。也可以制定成:[name].[ext]
表示原来的名字和文件后缀。publicPath
这里的 publicPath 与 output 中的 publicPath 一样,在这里指定后,会覆盖原有的 output.publicPath。比如:
rules: [ { test: /.(png|jpg|gif)/, use: { loader: "file-loader", options: { name: '[name].[ext]', publicPath: "", } } } ]
url-loader
与 file-loader
作用类似,唯一的不同是:url-loader
可以设置一个文件大小的阈(yù)值。当大于该阈值时与 file-loader 一样返回 publicPath,而小于阈值时则返回文件的 base64
形式编码。比如:
{ test: /.(png|jpg|gif)$/, use: { loader: "url-loader", options: { // 当文件小于这个值时,使用 base64 编码形式 // 大于该值时,使用 publicPath // 这个属性在 file-loader 中是没有的。 limit: 10240, name: '[name].[ext]' } } }
ts-loader
使用 ts-loader
可以让我们使用 typescript 来编写 js 代码。安装该 loader 后,还要安装 typescript。
yarn add ts-loader typescript
rules: [ { test: /.ts$/, use: "ts-loader", } ]
babel-loader
babel-loader 很重要,使用 babel-loader 可以让我们写的 JS 代码更加兼容浏览器环境。配置 babel-loader 时需要下载好几个其他的包。yarn add babel-loader @babel/core @babel/preset-env -D
。这三个是最核心的模块。主要作用如下:
babel-loader
它是 babel 与 webpack 协同工作的模块;@babel/core
babel 编译器的核心模块;@babel/preset-env
它是官方推荐的预置器,可根据用户设置的目标环境自动添加所需的插件和补丁来编译 ES6+ 代码。 具体配置如下:
rules: [ { test: /.js$/, // 不要编译 node_modules 下面的代码 exclude: path.join(__dirname,'../node_modules'), use: { loader: "babel-loader", options: { // 当为 true 时,会启动缓存机制, // 在重复打包未改变过的模块时防止二次编译 // 这样做可以加快打包速度 "cacheDirectory": true, } } } ]
对于 options 其它部分,可以在项目根目录下新建一个 .babelrc
文件。.babelrc 文件相当于一个 json 文件。它的配置项大概是这样的:
{ "presets": [], "plugins": [], }
比如要配置的一个内容:
{ "presets": [ ["@babel/env", // 每一个 preset 就是数组的每一项 // 当有的 preset 需要配置时,这一项将也是一个数组 // 数组的第一项是 preset 名称,第二项是该 preset 的配置内容,是一个对象 { // @babel/preset-env 会将 ES6 module 转成 CommonJS 的形式 // 将 mudules 设置成 false,可以禁止模块语句的转化 // 而将 ES6 module 的语法交给 webpack 本身处理 "mudules": false, // targets 可以指定兼容的各个环境的最低版本 "targets": { "edge": "17", "firefox": "60", "chrome": "67", "safari": "11.1" } } ] ], "plugins": [] }
env 的 targets 属性,可以配置的环境名称有:
chrome
,opera
,edge
,firefox
,safari
,ie
,ios
,android
,node
,electron
。当然 targets 的值也可以是一个字符串,例如:"targets": "> 0.25%, not dead"
表示仅包含浏览器具有> 0.25%市场份额的用户所需的 polyfill 和代码转换。
处理 react jsx 语法:@babel/preset-react
下载: yarn add @babel/preset-react -D
。当然,如果想使用 react,也要下载。在 .babelrc
的 presets 项中添加一个 preset:
{ "presets": [ ["@babel/env", { "modules": false, "targets": { "ie": 9 } } ], "@babel/react" ] }
这个时候就可以愉快的使用 react 了!
处理 .jsx
的文件
用 react 写的文件不光可以使用 .js
后缀,也可以使用 .jsx
文件后缀。但想要使用,这需要配置,不然会报错。来到 webpack 配置文件,添加一个 loader 项:
{ test: /.jsx$/, use: "babel-loader", }
当然,也可以与 js 配置写在一起:
test: /.(js|jsx)$/, use: { // ... }
postcss-loader
下载:npm install postcss-loader
配置:
// 不需要再次创建新的 loader 对象,应该在之前的 style-loader css-loader 之后直接添加 postcss-loader 即可 { test: /.css$/, // 顺序很重要 use: ['style-loader','css-loader','postcss-loader'], }
配置 PostCSS
这里需要创建一个文件 —— postcss.config.js
在项目根目录下。
自动添加后缀 —— autoprefixer
const autoprefixer = require('autoprefixer'); module.exports = { plugins: [ autoprefixer({ // 需要支持的特性(这里添加了 grid 布局) grid: true, // 浏览器兼容 overrideBrowserList: [ '>1%', // 浏览器份额 大于 1% 的。 'last 3 versions', // 兼容最后三个版本 'android 4.2', 'ie 8' ], }) ] };
postcss-preset-env
插件
这个插件可以让我们在应用中使用最新的 CSS 语法特性。同样需要下载: yarn add postcss-preset-env
。使用:
// postcss.config.js const autoprefixer = require('autoprefixer'); const postcssPresetEnv = require('postcss-preset-env'); module.exports = { plugins: [ autoprefixer({ grid: true, overrideBrowserList: [ '>1%', 'last 3 versions', 'android 4.2', 'ie 8' ], }), postcssPresetEnv({ state: 3, features: { 'color-mod-function': { unresolved: 'warn' }, browsers: 'last 2 versions' } }) ] };
resolve 配置项
这是一个可选的配置项,配置 resolve
用来设置模块如何被解析。几个常见的配置项:
1. resolve.alias
这个属性是给路径添加别名的,当使用 import
或者 require
去引用别的模块时,文件路径可能会比较长,这个时候就可以使用 alias
来简化路径。也可以在给定对象的键后的末尾添加 $,以表示精准匹配。比如:
// 在 webpack 中配置 resolve.alias module.exports = { // .... resolve: { alias: { xyz$: path.resolve(__dirname, 'path/to/file.js'), xyz: path.resolve(__dirname, 'path/to/file.js') } } // .... } // 引用:index.js import App1 from 'xyz'; // 精准匹配,会解析到 path/to/file.js 中的 js 文件 import App2 from 'xyz/index.js'; // 非精准匹配,匹配 path/to/file.js/index.js 中的内容
resolve.extensions
这个配置项设置后会自动解析确定的扩展。默认值为 extensions: ['.wasm', '.mjs', '.js', '.json']
。还可以做更改,比如 添加 jsx 文件:
{ resolve: { extensions: ['.wasm', '.mjs', '.js', '.json','.jsx'] } }
resolve 配置项还有许多,上面两个应该是比较常用的。其他的可以参看官网:webpack 中文文档[1] 或 webpack 英文文档[2]
最后
下一节还是 webpack 的配置,之后将要配置 devServer
、配置优化和生产环境的配置。最后再说一下 create-react-app 脚手架的改造。
参考资料
[1]
webpack中文文档: https://webpack.docschina.org/configuration/resolve/
[2]
webpack英文文档: https://webpack.js.org/configuration/resolve/