继 24 个实例入门并掌握「Webpack4」(二) 后续:html
demo17 源码地址vue
本节使用 demo15 的代码为基础node
咱们来模拟平时开发中,将打包完的代码防止到服务器上的操做,首先打包代码 npm run build
react
而后安装一个插件 npm i http-server -D
jquery
在 package.json 中配置一个 script 命令linux
{ "scripts": { "start": "http-server dist", "dev": "webpack-dev-server --open --config ./build/webpack.dev.conf.js", "build": "webpack --config ./build/webpack.prod.conf.js" } }
运行 npm run start
webpack
如今就起了一个服务,端口是 8080,如今访问 http://127.0.0.1:8080 就能看到效果了ios
若是你有在跑别的项目,端口也是 8080,端口就冲突,记得先关闭其余项目的 8080 端口,再
npm run start
咱们按 ctrl + c 关闭 http-server 来模拟服务器挂了的场景,再访问 http://127.0.0.1:8080 就会是这样git
页面访问不到了,由于咱们服务器挂了,PWA 是什么技术呢,它能够在你第一次访问成功的时候,作一个缓存,当服务器挂了以后,你依然可以访问这个网页es6
首先安装一个插件:workbox-webpack-plugin
npm i workbox-webpack-plugin -D
只有要上线的代码,才须要作 PWA 的处理,打开 webpack.prod.conf.js
const WorkboxPlugin = require('workbox-webpack-plugin') // 引入 PWA 插件 const prodConfig = { plugins: [ // 配置 PWA new WorkboxPlugin.GenerateSW({ clientsClaim: true, skipWaiting: true }) ] }
从新打包,在 dist 目录下会多出 service-worker.js
和 precache-manifest.js
两个文件,经过这两个文件就能使咱们的网页支持 PWA 技术,service-worker.js 能够理解为另类的缓存
还须要去业务代码中使用 service-worker
在 app.js 中加上如下代码
// 判断该浏览器支不支持 serviceWorker if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker .register('/service-worker.js') .then(registration => { console.log('service-worker registed') }) .catch(error => { console.log('service-worker registed error') }) }) }
从新打包,而后运行 npm run start
来模拟服务器上的操做,最好用无痕模式打开 http://127.0.0.1:8080 ,打开控制台
如今文件已经被缓存住了,再按 ctrl + c 关闭服务,再次刷新页面也仍是能显示的
TypeScript 是 JavaScript 类型的超集,它能够编译成纯 JavaScript
新建文件夹,npm init -y
,npm i webpack webpack-cli -D
,新建 src 目录,建立 index.ts 文件,这段代码在浏览器上是运行不了的,须要咱们打包编译,转成 js
class Greeter { greeting: string constructor(message: string) { this.greeting = message } greet() { return 'Hello, ' + this.greeting } } let greeter = new Greeter('world') alert(greeter.greet())
npm i ts-loader typescript -D
新建 webpack.config.js 并配置
const path = require('path') module.exports = { mode: 'production', entry: './src/index.ts', module: { rules: [ { test: /\.ts?$/, use: 'ts-loader', exclude: /node_modules/ } ] }, output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') } }
在 package.json 中配置 script
{ "scripts": { "build": "webpack" } }
运行 npm ruh build
,报错了,缺乏 tsconfig.json 文件
当打包 typescript 文件的时候,须要在项目的根目录下建立一个 tsconfig.json 文件
如下为简单配置,更多详情看官网
{ "compileerOptions": { "outDir": "./dist", // 写不写都行 "module": "es6", // 用 es6 模块引入 import "target": "es5", // 打包成 es5 "allowJs": true // 容许在 ts 中也能引入 js 的文件 } }
再次打包,打开 bundle.js 文件,将代码所有拷贝到浏览器控制台上,使用这段代码,能够看到弹窗出现 Hello,world,说明 ts 编译打包成功
npm i lodash
import _ from 'lodash' class Greeter { greeting: string constructor(message: string) { this.greeting = message } greet() { return _.join() } } let greeter = new Greeter('world') alert(greeter.greet())
lodash 的 join 方法须要咱们传递参数,可是如今咱们什么都没传,也没有报错,咱们使用 typescript 就是为了类型检查,在引入第三方库的时候也能如此,但是如今缺并无报错或者提示
咱们还要安装一个 lodash 的 typescript 插件,这样就能识别 lodash 方法中的参数,一旦使用的不对就会报错出来
npm i @types/lodash -D
安装完之后能够发现下划线 _ 报错了
须要改为 import * as _ from 'lodash'
,将 join 方法传递的参数删除,还能够发现 join 方法的报错,这就体现了 typescript 的优点,同理,引入 jQuery 也要引入一个 jQuery 对应的类型插件
如何知道使用的库须要安装对应的类型插件呢?
打开TypeSearch,在这里对应的去搜索你想用的库有没有类型插件,若是有只须要 npm i @types/jquery -D
便可
建立一个空文件夹,npm init -y
,npm webpack webpack-cli -D
起手式,以后安装 eslint 依赖
npm i eslint -D
使用 npx 运行此项目中的 eslint 来初始化配置,npx eslint --init
这里会有选择是 React/Vue/JavaScript,咱们统一都先选择 JavaScript。选完后会在项目的根目录下新建一个 .eslintrc.js
配置文件
module.exports = { env: { browser: true, es6: true }, extends: 'eslint:recommended', globals: { Atomics: 'readonly', SharedArrayBuffer: 'readonly' }, parserOptions: { ecmaVersion: 2018, sourceType: 'module' }, rules: {} }
里面就是 eslint 的一些规范,也能够定义一些规则,具体看 eslint 配置规则
在 index.js 中随便写点代码来测试一下 eslint
eslint 报错提示,变量定义后却没有使用,若是在编辑器里没出现报错提示,须要在 vscode 里先安装一个 eslint 扩展,它会根据你当前目录的下的 .eslintrc.js
文件来作做为校验的规则
也能够经过命令行的形式,让 eslint 校验整个 src 目录下的文件
若是你以为某个规则很麻烦,想屏蔽掉某个规则的时候,能够这样,根据 eslint 的报错提示,好比上面的 no-unused-vars
,将这条规则复制一下,在 .eslintrc.js
中的 rules 里配置一下,"no-unused-vars": 0
,0 表示禁用,保存后,就不会报错了,可是这种方式是适用于全局的配置,若是你只想在某一行代码上屏蔽掉 eslint 校验,能够这样作
/* eslint-disable no-unused-vars */ let a = '1'
这个 eslint 的 vscode 扩展和 webpack 是没有什么关联的,咱们如今要讲的是如何在 webpack 里使用 eslint,首先安装一个插件
npm i eslint-loader -D
在 webpack.config.js 中进行配置
/* eslint-disable no-undef */ // eslint-disable-next-line no-undef const path = require('path') module.exports = { mode: 'production', entry: { app: './src/index.js' // 须要打包的文件入口 }, module: { rules: [ { test: /\.js$/, // 使用正则来匹配 js 文件 exclude: /nodes_modules/, // 排除依赖包文件夹 use: { loader: 'eslint-loader' // 使用 eslint-loader } } ] }, output: { // eslint-disable-next-line no-undef publicPath: __dirname + '/dist/', // js 引用的路径或者 CDN 地址 // eslint-disable-next-line no-undef path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录 filename: 'bundle.js' // 打包后生产的 js 文件 } }
因为 webpack 配置文件也会被 eslint 校验,这里我先写上注释,关闭校验
若是你有使用 babel-loader 来转译,则 loader 应该这么写
loader: ['babel-loader', 'eslint-loader']
rules 的执行顺序是从右往左,从下往上的,先通过 eslint 校验判断代码是否符合规范,而后再经过 babel 来作转移
配置完 webpack.config.js,咱们将 index.js 还原回以前报错的状态,不要使用注释关闭校验,而后运行打包命令,记得去 package.json 配置 script
会在打包的时候,提示代码不合格,不只仅是生产环境,开发环境也能够配置,能够将 eslint-loader 配置到 webpack 的公共模块中,这样更有利于咱们检查代码规范
如:设置 fix 为 true,它会帮你自动修复一些错误,不能自动修复的,仍是须要你本身手动修复
{ loader: 'eslint-loader', // 使用 eslint-loader options: { fix: true } }
关于 eslint-loader,webpack 的官网也给出了配置,感兴趣的朋友本身去看一看
本节使用 demo15 的代码为基础
咱们先安装一个 lodash 插件 npm i lodash
,并在 app.js 文件中写入
import _ from 'lodash' console.log(_.join(['hello', 'world'], '-'))
在 build 文件夹下新建 webpack.dll.js 文件
const path = require('path') module.exports = { mode: 'production', entry: { vendors: ['lodash', 'jquery'] }, output: { filename: '[name].dll.js', path: path.resolve(__dirname, '../dll'), library: '[name]' } }
这里使用 library,忘记的朋友能够回顾一下第十六节,自定义函数库里的内容,定义了 library 就至关于挂载了这个全局变量,只要在控制台输入全局变量的名称就能够显示里面的内容,好比这里咱们是 library: '[name]'
对应的 name 就是咱们在 entry 里定义的 vendors
在 package.json 中的 script 再新增一个命令
{ "scripts": { "dev": "webpack-dev-server --open --config ./build/webpack.dev.conf.js", "build": "webpack --config ./build/webpack.prod.conf.js", "build:dll": "webpack --config ./build/webpack.dll.js" } }
运行 npm run build:dll
,会生成 dll 文件夹,而且文件为 vendors.dll.js
打开文件能够发现 lodash 已经被打包到了 dll 文件中
那咱们要如何使用这个 vendors.dll.js 文件呢
须要再安装一个依赖 npm i add-asset-html-webpack-plugin
,它会将咱们打包后的 dll.js 文件注入到咱们生成的 index.html 中
在 webpack.base.conf.js 文件中引入
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin') module.exports = { plugins: [ new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, '../dll/vendors.dll.js') // 对应的 dll 文件路径 }) ] }
使用 npm run dev
来打开网页
如今咱们已经把第三方模块单独打包成了 dll 文件,并使用
可是如今使用第三方模块的时候,要用 dll 文件,而不是使用 /node_modules/ 中的库,继续来修改 webpack.dll.js 配置
const path = require('path') const webpack = require('webpack') module.exports = { mode: 'production', entry: { vendors: ['lodash', 'jquery'] }, output: { filename: '[name].dll.js', path: path.resolve(__dirname, '../dll'), library: '[name]' }, plugins: [ new webpack.DllPlugin({ name: '[name]', // 用这个插件来分析打包后的这个库,把库里的第三方映射关系放在了这个 json 的文件下,这个文件在 dll 目录下 path: path.resolve(__dirname, '../dll/[name].manifest.json') }) ] }
保存后从新打包 dll,npm run build:dll
修改 webpack.base.conf.js 文件,添加 webpack.DllReferencePlugin 插件
module.exports = { plugins: [ // 引入咱们打包后的映射文件 new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, '../dll/vendors.manifest.json') }) ] }
以后再 webpack 打包的时候,就能够结合以前的全局变量 vendors 和 这个新生成的 vendors.manifest.json 映射文件,而后来对咱们的源代码进行分析,一旦分析出使用第三方库是在 vendors.dll.js 里,就会去使用 vendors.dll.js,不会去使用 /node_modules/ 里的第三方库了
再次打包 npm run build
,能够把 webpack.DllReferencePlugin 模块注释后再打包对比一下
注释前 4000ms 左右,注释后 4300ms 左右,虽然只是快了 300ms,可是咱们目前只是实验性的 demo,实际项目中,好比拿 vue 来讲,vue,vue-router,vuex,element-ui,axios 等第三方库均可以打包到 dll.js 里,那个时候的打包速度就能提高不少了
还能够继续拆分,修改 webpack.dll.js 文件
const path = require('path') const webpack = require('webpack') module.exports = { mode: 'production', entry: { lodash: ['lodash'], jquery: ['jquery'] }, output: { filename: '[name].dll.js', path: path.resolve(__dirname, '../dll'), library: '[name]' }, plugins: [ new webpack.DllPlugin({ name: '[name]', path: path.resolve(__dirname, '../dll/[name].manifest.json') // 用这个插件来分析打包后的这个库,把库里的第三方映射关系放在了这个 json 的文件下,这个文件在 dll 目录下 }) ] }
运行 npm run build:dll
能够把以前打包的 vendors.dll.js 和 vendors.manifest.json 映射文件给删除掉
而后再修改 webpack.base.conf.js
module.exports = { plugins: [ new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, '../dll/lodash.dll.js') }), new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, '../dll/jquery.dll.js') }), new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, '../dll/lodash.manifest.json') }), new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, '../dll/jquery.manifest.json') }) ] }
保存后运行 npm run dev
,看看能不能成功运行
这还只是拆分了两个第三方模块,就要一个个配置过去,有没有什么办法能简便一点呢? 有!
这里使用 node 的 api,fs 模块来读取文件夹里的内容,建立一个 plugins 数组用来存放公共的插件
const fs = require('fs') const plugins = [ // 开发环境和生产环境两者均须要的插件 new HtmlWebpackPlugin({ title: 'webpack4 实战', filename: 'index.html', template: path.resolve(__dirname, '..', 'index.html'), minify: { collapseWhitespace: true } }), new webpack.ProvidePlugin({ $: 'jquery' }) ] const files = fs.readdirSync(path.resolve(__dirname, '../dll')) console.log(files)
写完能够先输出一下,把 plugins 给注释掉,npm run build
打包看看输出的内容,能够看到文件夹中的内容以数组的形式被打印出来了,以后咱们对这个数组作一些循环操做就好了
完整代码:
const path = require('path') const fs = require('fs') const webpack = require('webpack') const HtmlWebpackPlugin = require('html-webpack-plugin') const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin') // 存放公共插件 const plugins = [ // 开发环境和生产环境两者均须要的插件 new HtmlWebpackPlugin({ title: 'webpack4 实战', filename: 'index.html', template: path.resolve(__dirname, '..', 'index.html'), minify: { collapseWhitespace: true } }), new webpack.ProvidePlugin({ $: 'jquery' }) ] // 自动引入 dll 中的文件 const files = fs.readdirSync(path.resolve(__dirname, '../dll')) files.forEach(file => { if (/.*\.dll.js/.test(file)) { plugins.push( new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, '../dll', file) }) ) } if (/.*\.manifest.json/.test(file)) { plugins.push( new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, '../dll', file) }) ) } }) module.exports = { entry: { app: './src/app.js' }, output: { path: path.resolve(__dirname, '..', 'dist') }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: [ { loader: 'babel-loader' } ] }, { test: /\.(png|jpg|jpeg|gif)$/, use: [ { loader: 'url-loader', options: { name: '[name]-[hash:5].min.[ext]', limit: 1000, // size <= 1KB outputPath: 'images/' } }, // img-loader for zip img { loader: 'image-webpack-loader', options: { // 压缩 jpg/jpeg 图片 mozjpeg: { progressive: true, quality: 65 // 压缩率 }, // 压缩 png 图片 pngquant: { quality: '65-90', speed: 4 } } } ] }, { test: /\.(eot|ttf|svg)$/, use: { loader: 'url-loader', options: { name: '[name]-[hash:5].min.[ext]', limit: 5000, // fonts file size <= 5KB, use 'base64'; else, output svg file publicPath: 'fonts/', outputPath: 'fonts/' } } } ] }, plugins, performance: false }
使用 npm run dev
打开网页也没有问题了,这样自动注入 dll 文件也搞定了,以后还要再打包第三方库只要添加到 webpack.dll.js 里面的 entry
属性中就能够了
本节使用 demo20 的代码为基础
在 src 目录下新建 list.js 文件,里面写 console.log('这里是 list 页面')
在 webpack.base.conf.js 中配置 entry,配置两个入口
module.exports = { entry: { app: './src/app.js', list: './src/list.js' } }
若是如今咱们直接 npm run build
打包,在打包自动生成的 index.html 文件中会发现 list.js 也被引入了,说明多入口打包成功,但并无实现多个页面的打包,我想打包出 index.html 和 list.html 两个页面,而且在 index.html 中引入 app.js,在 list.html 中引入 list.js,该怎么作?
为了方便演示,先将 webpack.prod.conf.js
中 cacheGroups
新增一个 default
属性,自定义 name
optimization: { splitChunks: { chunks: 'all', cacheGroups: { jquery: { name: 'jquery', // 单独将 jquery 拆包 priority: 15, test: /[\\/]node_modules[\\/]jquery[\\/]/ }, vendors: { test: /[\\/]node_modules[\\/]/, name: 'vendors' }, default: { name: 'code-segment' } } } }
打开 webpack.base.conf.js
文件,将 HtmlWebpackPlugin
拷贝一份,使用 chunks
属性,将须要打包的模块对应写入
// 存放公共插件 const plugins = [ new HtmlWebpackPlugin({ title: 'webpack4 实战', filename: 'index.html', template: path.resolve(__dirname, '..', 'index.html'), chunks: ['app', 'vendors', 'code-segment', 'jquery', 'lodash'] }), new HtmlWebpackPlugin({ title: '多页面打包', filename: 'list.html', template: path.resolve(__dirname, '..', 'index.html'), chunks: ['list', 'vendors', 'code-segment', 'jquery', 'lodash'] }), new CleanWebpackPlugin(), new webpack.ProvidePlugin({ $: 'jquery' }) ]
打包后的 dist 目录下生成了两个 html
打开 index.html 能够看到引入的是 app.js,而 list.html 引入的是 list.js,这就是 HtmlWebpackPlugin
插件的 chunks
属性,自定义引入的 js
若是要打包三个页面,再去 copy HtmlWebpackPlugin
,经过在 entry 中配置,若是有四个,五个,这样手动的复制就比较麻烦了,能够写个方法自动生成 HtmlWebpackPlugin
配置
修改 webpack.base.conf.js
const path = require('path') const fs = require('fs') const webpack = require('webpack') const HtmlWebpackPlugin = require('html-webpack-plugin') const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin') const CleanWebpackPlugin = require('clean-webpack-plugin') const makePlugins = configs => { // 基础插件 const plugins = [ new CleanWebpackPlugin(), new webpack.ProvidePlugin({ $: 'jquery' }) ] // 根据 entry 自动生成 HtmlWebpackPlugin 配置,配置多页面 Object.keys(configs.entry).forEach(item => { plugins.push( new HtmlWebpackPlugin({ title: '多页面配置', template: path.resolve(__dirname, '..', 'index.html'), filename: `${item}.html`, chunks: [item, 'vendors', 'code-segment', 'jquery', 'lodash'] }) ) }) // 自动引入 dll 中的文件 const files = fs.readdirSync(path.resolve(__dirname, '../dll')) files.forEach(file => { if (/.*\.dll.js/.test(file)) { plugins.push( new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, '../dll', file) }) ) } if (/.*\.manifest.json/.test(file)) { plugins.push( new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, '../dll', file) }) ) } }) return plugins } const configs = { entry: { index: './src/app.js', list: './src/list.js' }, output: { path: path.resolve(__dirname, '..', 'dist') }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: [ { loader: 'babel-loader' } ] }, { test: /\.(png|jpg|jpeg|gif)$/, use: [ { loader: 'url-loader', options: { name: '[name]-[hash:5].min.[ext]', limit: 1000, // size <= 1KB outputPath: 'images/' } }, // img-loader for zip img { loader: 'image-webpack-loader', options: { // 压缩 jpg/jpeg 图片 mozjpeg: { progressive: true, quality: 65 // 压缩率 }, // 压缩 png 图片 pngquant: { quality: '65-90', speed: 4 } } } ] }, { test: /\.(eot|ttf|svg)$/, use: { loader: 'url-loader', options: { name: '[name]-[hash:5].min.[ext]', limit: 5000, // fonts file size <= 5KB, use 'base64'; else, output svg file publicPath: 'fonts/', outputPath: 'fonts/' } } } ] }, performance: false } makePlugins(configs) configs.plugins = makePlugins(configs) module.exports = configs
再次打包后效果相同,若是还要增长页面,只要在 entry 中再引入一个 js 文件做为入口便可
多页面配置其实就是定义多个 entry,配合 htmlWebpackPlugin 生成多个 html 页面
新建文件夹,npm init -y
,npm i webpack webpack-cli -D
,新建 src/index.js,写入 console.log('hello world')
新建 loaders/replaceLoader.js
文件
module.exports = function(source) { return source.replace('world', 'loader') }
source 参数就是咱们的源代码,这里是将源码中的 world 替换成 loader
新建 webpack.config.js
const path = require('path') module.exports = { mode: 'development', entry: { main: './src/index.js' }, module: { rules: [ { test: /.js/, use: [path.resolve(__dirname, './loaders/replaceLoader.js')] // 引入自定义 loader } ] }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
目录结构:
打包后打开 dist/main.js 文件,在最底部能够看到 world 已经被改成了 loader,一个最简单的 loader 就写完了
添加 optiions 属性
const path = require('path') module.exports = { mode: 'development', entry: { main: './src/index.js' }, module: { rules: [ { test: /.js/, use: [ { loader: path.resolve(__dirname, './loaders/replaceLoader.js'), options: { name: 'xh' } } ] } ] }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
修改 replaceLoader.js 文件,保存后打包,输出看看效果
module.exports = function(source) { console.log(this.query) return source.replace('world', this.query.name) }
打包后生成的文件也改成了 options 中定义的 name
更多的配置见官网 API,找到 Loader Interface,里面有个 this.query
若是你的 options 不是一个对象,而是按字符串形式写的话,可能会有一些问题,这里官方推荐使用 loader-utils 来获取 options 中的内容
安装 npm i loader-utils -D
,修改 replaceLoader.js
const loaderUtils = require('loader-utils') module.exports = function(source) { const options = loaderUtils.getOptions(this) console.log(options) return source.replace('world', options.name) }
console.log(options)
与 console.log(this.query)
输出内容一致
若是你想传递额外的信息出去,return 就很差用了,官网给咱们提供了 this.callback API,用法以下
this.callback( err: Error | null, content: string | Buffer, sourceMap?: SourceMap, meta?: any )
修改 replaceLoader.js
const loaderUtils = require('loader-utils') module.exports = function(source) { const options = loaderUtils.getOptions(this) const result = source.replace('world', options.name) this.callback(null, result) }
目前没有用到 sourceMap(必须是此模块可解析的源映射)、meta(能够是任何内容(例如一些元数据)) 这两个可选参数,只将 result 返回回去,保存从新打包后,效果和 return 是同样的
若是在 loader 中写异步代码,会怎么样
const loaderUtils = require('loader-utils') module.exports = function(source) { const options = loaderUtils.getOptions(this) setTimeout(() => { const result = source.replace('world', options.name) return result }, 1000) }
报错 loader 没有返回,这里使用 this.async 来写异步代码
const loaderUtils = require('loader-utils') module.exports = function(source) { const options = loaderUtils.getOptions(this) const callback = this.async() setTimeout(() => { const result = source.replace('world', options.name) callback(null, result) }, 1000) }
模拟一个同步 loader 和一个异步 loader
新建一个 replaceLoaderAsync.js
文件,将以前写的异步代码放入,修改 replaceLoader.js
为同步代码
// replaceLoaderAsync.js const loaderUtils = require('loader-utils') module.exports = function(source) { const options = loaderUtils.getOptions(this) const callback = this.async() setTimeout(() => { const result = source.replace('world', options.name) callback(null, result) }, 1000) } // replaceLoader.js module.exports = function(source) { return source.replace('xh', 'world') }
修改 webpack.config.js
,loader 的执行顺序是从下到上,先执行异步代码,将 world 改成 xh,再执行同步代码,将 xh 改成 world
module: { rules: [ { test: /.js/, use: [ { loader: path.resolve(__dirname, './loaders/replaceLoader.js') }, { loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js'), options: { name: 'xh' } } ] } ] }
保存后打包,在 mian.js 中能够看到已经改成了 hello world
,使用多个 loader 也完成了
若是有多个自定义 loader,每次都经过 path.resolve(__dirname, xxx)
这种方式去写,有没有更好的方法?
使用 resolveLoader
,定义 modules,当你使用 loader 的时候,会先去 node_modules
中去找,若是没找到就会去 ./loaders
中找
const path = require('path') module.exports = { mode: 'development', entry: { main: './src/index.js' }, resolveLoader: { modules: ['node_modules', './loaders'] }, module: { rules: [ { test: /.js/, use: [ { loader: 'replaceLoader.js' }, { loader: 'replaceLoaderAsync.js', options: { name: 'xh' } } ] } ] }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
首先新建一个文件夹,npm 起手式操做一番,具体的在前几节已经说了,再也不赘述
在根目录下新建 plugins 文件夹,新建 copyright-webpack-plugin.js
,通常咱们用的都是 xxx-webpack-plugin
,因此咱们命名也按这样来,plugin 的定义是一个类
class CopyrightWebpackPlugin { constructor() { console.log('插件被使用了') } apply(compiler) {} } module.exports = CopyrightWebpackPlugin
在 webpack.config.js 中使用,因此每次使用 plugin 都要使用 new,由于本质上 plugin 是一个类
const path = require('path') const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin') module.exports = { mode: 'development', entry: { main: './src/index.js' }, plugins: [new CopyrightWebpackPlugin()], output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
保存后打包,插件被使用了,只不过咱们什么都没干
若是咱们要传递参数,能够这样
new CopyrightWebpackPlugin({ name: 'xh' })
同时在 copyright-webpack-plugin.js
中接收
class CopyrightWebpackPlugin { constructor(options) { console.log('插件被使用了') console.log('options = ', options) } apply(compiler) {} } module.exports = CopyrightWebpackPlugin
咱们先把 constructor 注释掉,在即将要把打包的结果,放入 dist 目录以前的这个时刻,咱们来作一些操做
apply(compiler) {}
compiler 能够看做是 webpack 的实例,具体见官网 compiler-hooks
hooks 是钩子,像 vue、react 的生命周期同样,找到 emit
这个时刻,将打包结果放入 dist 目录前执行,这里是个 AsyncSeriesHook
异步方法
class CopyrightWebpackPlugin { apply(compiler) { compiler.hooks.emit.tapAsync( 'CopyrightWebpackPlugin', (compilation, cb) => { console.log(11) cb() } ) } } module.exports = CopyrightWebpackPlugin
由于 emit 是异步的,能够经过 tapAsync 来写,当要把代码放入到 dist 目录以前,就会触发这个钩子,走到咱们定义的函数里,若是你用 tapAsync 函数,记得最后要用 cb() ,tapAsync 要传递两个参数,第一个参数传递咱们定义的插件名称
保存后再次打包,咱们写的内容也输出了
compilation 这个参数里存放了此次打包的全部内容,能够输出一下 compilation.assets
看一下
返回结果是一个对象,main.js
是 key,也就是打包后生成的文件名及文件后缀,咱们能够来仿照一下
class CopyrightWebpackPlugin { apply(compiler) { compiler.hooks.emit.tapAsync( 'CopyrightWebpackPlugin', (compilation, cb) => { // 生成一个 copyright.txt 文件 compilation.assets['copyright.txt'] = { source: function() { return 'copyright by xh' }, size: function() { return 15 // 上面 source 返回的字符长度 } } console.log('compilation.assets = ', compilation.assets) cb() } ) } } module.exports = CopyrightWebpackPlugin
在 dist 目录下生成了 copyright.txt
文件
以前介绍的是异步钩子,如今使用同步钩子
class CopyrightWebpackPlugin { apply(compiler) { // 同步钩子 compiler.hooks.compile.tap('CopyrightWebpackPlugin', compilation => { console.log('compile') }) // 异步钩子 compiler.hooks.emit.tapAsync( 'CopyrightWebpackPlugin', (compilation, cb) => { compilation.assets['copyright.txt'] = { source: function() { return 'copyright by xh' }, size: function() { return 15 // 字符长度 } } console.log('compilation.assets = ', compilation.assets) cb() } ) } } module.exports = CopyrightWebpackPlugin
在 src 目录下新建三个文件 word.js
、message.js
、index.js
,对应的代码:
// word.js export const word = 'hello' // message.js import { word } from './word.js' const message = `say ${word}` export default message // index.js import message from './message.js' console.log(message)
新建 bundle.js
const fs = require('fs') const moduleAnalyser = filename => { const content = fs.readFileSync(filename, 'utf-8') console.log(content) } moduleAnalyser('./src/index.js')
使用 node 的 fs 模块,读取文件信息,并在控制台输出,这里全局安装一个插件,来显示代码高亮,npm i cli-highlight -g
,运行 node bundle.js | highlight
index.js 中的代码已经被输出到控制台上,并且代码有高亮,方便阅读,读取入口文件信息就完成了
如今咱们要读取 index.js 文件中使用的 message.js 依赖,import message from './message.js'
安装一个第三方插件 npm i @babel/parser
@babel/parser 是 Babel 中使用的 JavaScript 解析器。
官网也提供了相应的示例代码,根据示例代码来仿照,修改咱们的文件
const fs = require('fs') const parser = require('@babel/parser') const moduleAnalyser = filename => { const content = fs.readFileSync(filename, 'utf-8') console.log( parser.parse(content, { sourceType: 'module' }) ) } moduleAnalyser('./src/index.js')
咱们使用的是 es6 的 module 语法,因此 sourceType: 'module'
保存后运行,输出了 AST (抽象语法树),里面有一个 body 字段,咱们输出这个字段
const fs = require('fs') const parser = require('@babel/parser') const moduleAnalyser = filename => { const content = fs.readFileSync(filename, 'utf-8') const ast = parser.parse(content, { sourceType: 'module' }) console.log(ast.program.body) } moduleAnalyser('./src/index.js')
打印出了两个 Node 节点,第一个节点的 type 是 ImportDeclaration(引入的声明),对照咱们在 index.js 中写的 import message from './message.js'
,第二个节点的 type 是 ExpressionStatement (表达式的声明),对照咱们写的 console.log(message)
使用 babel 来帮咱们生成抽象语法树,咱们再导入 import message1 from './message1.js'
再运行
抽象语法树将咱们的 js 代码转成了对象的形式,如今就能够遍历抽象语法树生成的节点对象中的 type,是否为 ImportDeclaration
,就能找到代码中引入的依赖了
再借助一个工具 npm i @babel/traverse
const fs = require('fs') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const moduleAnalyser = filename => { const content = fs.readFileSync(filename, 'utf-8') const ast = parser.parse(content, { sourceType: 'module' }) traverse(ast, { ImportDeclaration({ node }) { console.log(node) } }) } moduleAnalyser('./src/index.js')
只打印了两个 ImportDeclaration,遍历结束,咱们只须要取到依赖的文件名,在打印的内容中,每一个节点都有个 source
属性,里面有个 value
字段,表示的就是文件路径及文件名
const fs = require('fs') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const moduleAnalyser = filename => { const content = fs.readFileSync(filename, 'utf-8') const ast = parser.parse(content, { sourceType: 'module' }) const dependencise = [] traverse(ast, { ImportDeclaration({ node }) { dependencise.push(node.source.value) } }) console.log(dependencise) } moduleAnalyser('./src/index.js')
保存完从新运行,输出结果:
['./message.js', './message1.js']
这样就对入口文件的依赖分析就分析出来了,如今把 index.js 中引入的 message1.js
的依赖给删除,这里有个注意点,打印出来的文件路径是相对路径,相对于 src/index.js
文件,可是咱们打包的时候不能是入口文件(index.js)的相对路径,而应该是根目录的相对路径(或者说是绝对路径),借助 node 的 api,引入一个 path
const fs = require('fs') const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const moduleAnalyser = filename => { const content = fs.readFileSync(filename, 'utf-8') const ast = parser.parse(content, { sourceType: 'module' }) const dependencise = [] traverse(ast, { ImportDeclaration({ node }) { const dirname = path.dirname(filename) console.log(dirname) dependencise.push(node.source.value) } }) // console.log(dependencise) } moduleAnalyser('./src/index.js')
输出为 ./src
,继续修改
ImportDeclaration({ node }) { const dirname = path.dirname(filename) const newFile = path.join(dirname, node.source.value) console.log(newFile) dependencise.push(node.source.value) }
输出为 src\message.js
windows 和 类 Unix(linux/mac),路径是有区别的。windows 是用反斜杠 分割目录或者文件的,而在类 Unix 的系统中是用的 /。
因为我是 windows 系统,因此这里输出为 src\message.js
,而类 Unix 输出的为 src/message.js
.\src\message.js
这个路径是咱们真正打包时要用到的路径
newFile .\src\message.js [ '.\\src\\message.js' ]
既存一个相对路径,又存一个绝对路径
const fs = require('fs') const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const moduleAnalyser = filename => { const content = fs.readFileSync(filename, 'utf-8') const ast = parser.parse(content, { sourceType: 'module' }) const dependencise = {} traverse(ast, { ImportDeclaration({ node }) { const dirname = path.dirname(filename) const newFile = '.\\' + path.join(dirname, node.source.value) console.log('newFile', newFile) dependencise[node.source.value] = newFile } }) console.log(dependencise) return { filename, dependencise } } moduleAnalyser('./src/index.js')
newFile .\src\message.js { './message.js': '.\\src\\message.js' }
由于咱们写的代码是 es6,浏览器没法识别,仍是须要 babel 来作转换
npm i @babel/core @babel/preset-env
'use strict' var _message = _interopRequireDefault(require('./message.js')) function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj } } console.log(_message.default)
const fs = require('fs') const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const babel = require('@babel/core') const moduleAnalyser = filename => { const content = fs.readFileSync(filename, 'utf-8') const ast = parser.parse(content, { sourceType: 'module' }) const dependencise = {} traverse(ast, { ImportDeclaration({ node }) { const dirname = path.dirname(filename) const newFile = '.\\' + path.join(dirname, node.source.value) dependencise[node.source.value] = newFile } }) const { code } = babel.transformFromAst(ast, null, { presets: ['@babel/preset-env'] }) return { filename, dependencise, code } } const moduleInfo = moduleAnalyser('./src/index.js') console.log(moduleInfo)
分析的结果就在控制台上打印了
{ filename: './src/index.js', dependencise: { './message.js': '.\\src\\message.js' }, code: '"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message.default);' }
目前咱们只对一个模块进行分析,接下来要对整个项目进行分析,因此咱们先分析了入口文件,再分析入口文件中所使用的依赖
建立一个函数来循环依赖并生成图谱
// 依赖图谱 const makeDependenciesGraph = entry => { const entryModule = moduleAnalyser(entry) const graphArray = [entryModule] for (let i = 0; i < graphArray.length; i++) { const item = graphArray[i] const { dependencise } = item // 若是入口文件有依赖就去作循环依赖,对每个依赖作分析 if (dependencise) { for (const j in dependencise) { if (dependencise.hasOwnProperty(j)) { graphArray.push(moduleAnalyser(dependencise[j])) } } } } console.log('graphArray = ', graphArray) }
将入口的依赖,依赖中的依赖所有都分析完放到 graphArray 中,控制台输出的打印结果
能够看到 graphArray 中一共有三个对象,就是咱们在项目中引入的三个文件,所有被分析出来了,为了方便阅读,咱们建立一个 graph 对象,将分析的结果依次放入
// 依赖图谱 const makeDependenciesGraph = entry => { const entryModule = moduleAnalyser(entry) const graphArray = [entryModule] for (let i = 0; i < graphArray.length; i++) { const item = graphArray[i] const { dependencise } = item // 若是入口文件有依赖就去作循环依赖,对每个依赖作分析 if (dependencise) { for (const j in dependencise) { if (dependencise.hasOwnProperty(j)) { graphArray.push(moduleAnalyser(dependencise[j])) } } } } // console.log('graphArray = ', graphArray) // 建立一个对象,将分析后的结果放入 const graph = {} graphArray.forEach(item => { graph[item.filename] = { dependencise: item.dependencise, code: item.code } }) console.log('graph = ', graph) return graph }
输出的 graph 为:
最后在 makeDependenciesGraph
函数中将 graph 返回,赋值给 graphInfo,输出的结果和 graph 是同样的
const graghInfo = makeDependenciesGraph('./src/index.js') console.log(graghInfo)
如今已经拿到了全部代码生成的结果,如今咱们借助 DependenciesGraph(依赖图谱) 来生成真正能在浏览器上运行的代码
最好放在一个大的闭包中来执行,避免污染全局环境
const generateCode = entry => { // makeDependenciesGraph 返回的是一个对象,须要转换成字符串 const graph = JSON.stringify(makeDependenciesGraph(entry)) return ` (function (graph) { })(${graph}) ` } const code = generateCode('./src/index.js') console.log(code)
我这里先把输出的 graph 代码格式化了一下,能够发如今 index.js
用到了 require
方法,message.js
中不只用了 require
方法,还用 exports
对象,可是在浏览器中,这些都是不存在的,若是咱们直接去执行,是会报错的
let graph = { './src/index.js': { dependencise: { './message.js': '.\\src\\message.js' }, code: ` "use strict";\n\n var _message = _interopRequireDefault(require("./message.js"));\n\n function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } \n\n console.log(_message.default); ` }, '.\\src\\message.js': { dependencise: { './word.js': '.\\src\\word.js' }, code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.default = void 0;\n\nvar _word = require("./word.js");\n\nvar message = "say ".concat(_word.word);\nvar _default = message;\nexports.default = _default;' }, '.\\src\\word.js': { dependencise: {}, code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.word = void 0;\nvar word = \'hello\';\nexports.word = word;' } }
接下来要去构造 require 方法和 exports 对象
const generateCode = entry => { console.log(makeDependenciesGraph(entry)) // makeDependenciesGraph 返回的是一个对象,须要转换成字符串 const graph = JSON.stringify(makeDependenciesGraph(entry)) return ` (function (graph) { // 定义 require 方法 function require(module) { }; require('${entry}') })(${graph}) ` } const code = generateCode('./src/index.js') console.log(code)
graph 是依赖图谱,拿到 entry 后去执行 ./src/index.js
中的 code,也就是下面高亮部分的代码,为了直观我把前面输出的 graph 代码拿下来参考:
let graph = { './src/index.js': { dependencise: { './message.js': '.\\src\\message.js' }, code: ` "use strict";\n\n var _message = _interopRequireDefault(require("./message.js"));\n\n function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } \n\n console.log(_message.default); ` } }
为了让 code 中的代码执行,这里再使用一个闭包,让每个模块里的代码放到闭包里来执行,这样模块的变量就不会影响到外部的变量
return ` (function (graph) { // 定义 require 方法 function require(module) { (function (code) { eval(code) })(graph[module].code) }; require('${entry}') })(${graph}) `
闭包里传递的是 graph[module].code
,如今 entry 也就是 ./src/index.js
这个文件,会传给 require 中的 module 变量,实际上去找依赖图谱中 ./src/index.js
对应的对象,而后再去找到 code 中对应的代码,也就是下面这段代码,被我格式化过,为了演示效果
'use strict' var _message = _interopRequireDefault(require('./message.js')) function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj } } console.log(_message.default)
可是咱们会发现,这里 _interopRequireDefault(require('./message.js'))
引入的是 ./message.js
相对路径,等到第二次执行的时候,require(module)
这里的 module
对应的就是 ./message.js
它会到 graph 中去找 ./message.js
下对应的 code,但是咱们在 graph 中存的是 '.\\src\\message.js'
绝对路径,这样就会找不到对象
由于咱们以前写代码的时候引入的是相对路径,如今咱们要把相对路径转换成绝对路径才能正确执行,定义一个 localRequire 方法,这样当下次去找的时候就会走咱们本身定义的 localRequire,其实就是一个相对路径转换的方法
return ` (function (graph) { // 定义 require 方法 function require(module) { // 相对路径转换 function localRequire(relativePath) { return require(graph[module].dependencise[relativePath]) } (function (require, code) { eval(code) })(localRequire, graph[module].code) }; require('${entry}') })(${graph}) `
咱们定义了 localRequire 方法,并把它传递到闭包里,当执行了 eval(code)
时执行了 require
方法,就不是执行外部的 require(module)
这个方法,而是执行咱们传递进去的 localRequire 方法
咱们在分析出的代码中是这样引入 message.js
的
var _message = _interopRequireDefault(require('./message.js'))
这里调用了 require('./message.js')
,就是咱们上面写的 require
方法,也就是 localRequire(relativePath)
因此 relativePath 就是 './message.js'
这个方法返回的是 require(graph[module].dependencise[relativePath])
这里我把参数带进去,就是这样:
graph('./src/index.js').dependencise['./message.js']
let graph = { './src/index.js': { dependencise: { './message.js': '.\\src\\message.js' }, code: ` "use strict";\n\n var _message = _interopRequireDefault(require("./message.js"));\n\n function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } \n\n console.log(_message.default); ` } }
对照着图谱就能发现最终返回的就是 '.\\src\\message.js'
绝对路径,返回绝对路径后,咱们再调用 require(graph('./src/index.js').dependencise['./message.js'])
就是执行外部定义的 require(module)
这个方法,从新递归的去执行,光这样还不够,这只是实现了 require 方法,还差 exports 对象,因此咱们再定义一个 exports 对象
return ` (function (graph) { // 定义 require 方法 function require(module) { // 相对路径转换 function localRequire(relativePath) { return require(graph[module].dependencise[relativePath]) } var exports = {}; (function (require, exports, code) { eval(code) })(localRequire, exports, graph[module].code) return exports }; require('${entry}') })(${graph}) `
最后要记得 return exports
将 exports 导出,这样下一个模块在引入这个模块的时候才能拿到导出的结果,如今代码生成的流程就写完了,最终返回的是一个大的字符串,保存再次运行 node bundle.js | highlight
这里我是 windows 环境,将输出完的代码直接放到浏览器里不行,我就把压缩的代码格式化成下面这种样子,再放到浏览器里就能输出成功了
;(function(graph) { function require(module) { function localRequire(relativePath) { return require(graph[module].dependencise[relativePath]) } var exports = {} ;(function(require, exports, code) { eval(code) })(localRequire, exports, graph[module].code) return exports } require('./src/index.js') })({ './src/index.js': { dependencise: { './message.js': '.\\src\\message.js' }, code: '"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message.default);' }, '.\\src\\message.js': { dependencise: { './word.js': '.\\src\\word.js' }, code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.default = void 0;\n\nvar _word = require("./word.js");\n\nvar message = "say ".concat(_word.word);\nvar _default = message;\nexports.default = _default;' }, '.\\src\\word.js': { dependencise: {}, code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.word = void 0;\nvar word = \'hello\';\nexports.word = word;' } })
将上面代码放入浏览器的控制台中,回车就能输出 say hello
这就是打包工具打包后的内容,期间涉及了 node 知识,使用 babel 来转译 ast(抽象语法树),最后的 generateCode 函数涉及到了递归和闭包,形参和实参,须要你们多看几遍,加深理解