目标是实现webpack打包零配置

时间:2022-12-09 18:03:24

目标是实现webpack打包零配置(配置在内部已默认设置,可重写覆盖)

创建脚手架cli

包括创建项目、本地运行项目、打包线上项目

基本功能

  • SPA/MPA 单选
  • Router、Vuex 多选
  • eslint 默认安装
  • 自动安装依赖,无需​​npm i​
  • 默认别名​​'@'​​ 指向​​'./src'​​ 如:​​import home from "@/pages/home"​

创建命令

新建一个项目如: test-cli,创建package.json, 在bin对象中定义命令名称及入口

package.json

"bin": {
"test-cli": "bin/cli.js"
}
​commander​

node.js 命令行界面的完整解决方案。

​npm commander​

bin/cli.js

#!/usr/bin/env node

// 定义指令
const program = require("commander");
program.command("create <project-name>")
.description("create a new project powered by test-cli")
.option("-f, --force", "Overwrite target directory if it exists")
.action((name, options) => {
require('../lib/create')(name, options) //添加执行文件
});
program.parse(process.argv);

/usr/bin/env就是告诉系统可以在PATH目录中查找。 所以配置#!/usr/bin/env node, 就是解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件

npm link

本地项目和本地npm模块之间建立连接,可以在本地进行模块测试

在test-cli根目录 执行npm link,这时就可以在终端中输入 test-cli create [项目名]命令

inquirer

一组常见的交互式命令行用户界面。

​npm inquirer​

prompts.js

module.exports = {
features: () => {
return [
{
name: "isSPA",
type: "list",
message: `create project SPA or MPA`,
choices: [
{ name: "SPA", value: PromptKey.isSPA.spa },
{ name: "MPA", value: PromptKey.isSPA.mpa },
],
},{
name: 'features',
type: 'checkbox',
when(anwser){
if(anwser.isSPA === PromptKey.isSPA.spa){
return true
} else {
chalk.red.bold(`MPA support future!!`)
exit(1);
return false
}
},
message: 'Check the features needed for your project:',
choices: [PgRouter, PgVuex],
pageSize: 10
}
];
},
async handle({anwsers, framework}){
let templatesPreDir = `${framework}/${anwsers.isSPA}/`
let result = {templates : [], plugins: [], templatesPreDir};
anwsers.features.map(f => {
let plugin = plugins[f]({value: PromptKey.features[f]});
f = f === PromptKey.features.vuex ? 'store' : f;
result.templates.push({
templatesPreDir,
path: templatesPreDir + f,
writeInMain: plugin.writeInMain
})
result.plugins.push({
dependencies: plugin.dependencies
})
})
return result
}
};

根据用户选择的命令处理模板

创建模板

在github或gitlab中创建一个vue(因该脚手架后面会增加vue构建及打包功能,所有选择vue)项目模板,模板根据自身需要创建目录

目标是实现webpack打包零配置

下载模板

把模板下载到当前目录

git-clone

通过 shell 命令克隆一个 git 存储库。 ​​npm git-clone​

loadRemote.js

const gitClone = require('git-clone')
const chalk = require('./chalk')

module.exports = async function loadRemote(repository, branch, filePath){

branch = branch || 'master'
// await fs.remove(filePath)
return await new Promise((resolve, reject) => {
if(!filePath){
chalk.bold.red('没有指定下载到目标目录')
reject()
}
gitClone(repository, filePath, { checkout: branch }, err => {
if (err) return reject(err)
resolve()
})
}).catch(err => {
console.log(err)
chalk.bold.red(`git clone error ${err && err.message}`)
})
}

chalk

终端字符串样式

​npm chalk​

删除.git文件 ​​fs.removeSync(targetDir + '/.git')​

ejs

嵌入式 JavaScript 模板

​npm ejs​

把prompts 用户选择的数据,传递到ejs模板中

import Vue from 'vue';
import App from './App.vue';
<% if (typeof router !== 'undefined') { %><%- router.import %><% } %>
<% if (typeof vuex !== 'undefined') { %><%- vuex.import%><% } %>

Vue.config.productionTip = false;

new Vue({
<% if (typeof router !== 'undefined') { %><%- router.vueOpt%><% } %>
<% if (typeof vuex !== 'undefined') { %><%- vuex.vueOpt%><% } %>
render: h => h(App),
}).$mount('#app');

把ejs返回的数据写入到模板的main.js中
​fs.writeFileSync(path.resolve(targetDir, './src/main.js'), str)​

prettier

美化代码

​npm prettier​

读取模板中的​​.prettierrc.js​​文件,进行格式化代码

format.js

const fs = require('fs-extra')
const path = require('path')
const prettier = require("prettier")

class Format {
constructor(targetDir){
this.targetDir = targetDir
this.formatOptions = {}

// 获取格式化文件
if(fs.existsSync(`${targetDir}/.prettierrc.js`)){
this.formatOptions = require(`${targetDir}/.prettierrc.js`)
}

}
run(_path){
let url
if(_path){
url = _path
}else{
url = this.targetDir
}
fs.readdir(url, (err, files) => {
if (err) {
console.log('err', err)
} else {
files.forEach ( (filename) => {
// 获取绝对路径
let filedir = path.join(url, filename)
fs.stat(filedir, (error, stats) => {
if (error) {
console.log(error)
} else {
// 文件夹、文件的不同处理
let isFile = stats.isFile()
let isDir = stats.isDirectory()

if (isFile) {
try{
let parser = 'babel'
const extname = path.extname(filedir)
if(extname === '.vue'){
parser = 'vue'
}else if(extname === '.html'){
parser = 'html'
}
const exts = ['.vue', '.js', 'html']
if(!exts.includes(extname)){
return
}

this.formatOptions.parser = parser
const formatTemp = prettier.format(fs.readFileSync(filedir, 'utf-8'), this.formatOptions)
fs.writeFileSync(filedir, formatTemp)
}catch(err){
console.log('写入文件失败!', err)
}
}

if (isDir) {
// 递归
this.run(filedir)
}
}
})
})
}
})
}
}

module.exports = Format;

install

execa

execa是可以调用shell和本地外部程序的javascript封装

​npm execa​

install.js

/**
* 自动化安装依赖包
*/
const execa = require('execa')
const ora = require('ora')
const { chalk } = require("../utils/index")

class Installer {
async run(targetDir){

const spinner = ora(chalk.blue('Loading install...')).start()

const command = 'npm'
const args = ['install', '--loglevel', 'error']
try{
await execa(command, args, {
cwd: targetDir,
stdio: ['inherit', 'inherit', 'inherit']
})
spinner.succeed(chalk.green('package install done!'))
}catch(e){
spinner.fail(chalk.red('package install fail!'))
}
spinner.stop()
}
}
module.exports = new Installer();
ora

优雅的终端旋转器

​npm ora​

create.js

const fs = require('fs-extra')
const path = require('path')
const { exit } = require('process');
const { chalk, loadRemote } = require("../utils/index");
const packageJson = require('../package.json')
const promptFactory = require('./prompts/promptFactory')
const moveTemplate = require('./moveTemplate');
const install = require('./install');
const Format = require('./format');

async function create (projectName, options) {
const cwd = options.cwd || process.cwd()
const targetDir = path.resolve(cwd, projectName)

const framework = 'vue'; // vue是test-project中的一个目录,之后可能会有react等模板,也会有react目录
const prompts = new promptFactory(framework, {targetDir, ...options});
await prompts.handlePrompts();

chalk.yellow('\nload remote source...')
await loadRemote(packageJson["test-project"].repository, framework, targetDir)
fs.removeSync(targetDir + '/.git')
chalk.yellow('\nload remote source done')

let tpls = prompts.getTemplates()
let templatesPreDir = prompts.getTemplatesPreDir()

await moveTemplate(tpls, targetDir, templatesPreDir).catch(err => {
chalk.red.bold(`move templates error ${err && err.message}`)
})

const format = new Format(targetDir)
format.run()

install.run(targetDir)
}
module.exports = (...args) => {
return create(...args).catch(err => {
console.log('create error', err)
process.exit(1)
})
}

cli 完成


cli-service

目标是实现webpack打包零配置

依赖于test.config.js(test-project)

基本功能

  • 支持vue Spa模式
  • 支持​​dev​​,​​test​​,​​build​​环境配置
  • 支持原生webpack配置
  • 支持js/css/image压缩
  • 支持配置静态资源cdn
  • 支持css/sass/less/stylus, 支持css module和css extract特性
  • 支持font引用
  • 支持eslint, postcss特性
  • build环境默认启动​​webpack-bundle-analyzer​

创建命令

program
.command('serve')
.description('start the development environment application')
.action(() => {
require('../cmd/dev')
})

program
.command('build')
.description('compiles the application for production deployment')
.option('-t, --test', 'start the test environment application')
.action((options) => {
require('../cmd/build')(options)
})

在上面的模板中(test-project) 读取test.config.js, 根据配置进行打包编译等处理

test.config.js

const path = require('path')
const resolve = (filePath) => path.resolve(process.cwd(), filePath)

module.exports = {
entry: './src/main.js',
dev: {
filename: '[name].bundle.js',
path: resolve('./dist'),
publicPath: '/'
},
build: {
filename: '[name].bundle.js',
path: resolve('./dist'),
// publicPath: '//www.baidu.com/',
publicPath: '/',
assetModuleFilename: 'images/[hash][ext][query]'
},
resolve: {},
module: {
rules: []
},
plugins: [],
devServer: {
proxy: {
'/api': {
target: 'http://www.baidu.com',
pathRewrite: { '^/api': '' },
changeOrigin: true
}
}
}
}

config

打包系统-默认配置,​​test.config.js​​可重写并覆盖

webpack.js

"use strict";

const path = require('path')

const baseDir = process.cwd()
const resolve = (filePath) => path.resolve(process.cwd(), filePath)

module.exports = {
context: baseDir,
entry: './src/main.js',
output: {
filename: '[name].bundle.[contenthash].js',
publicPath: '/',
path: resolve('./dist'),
assetModuleFilename: 'images/[hash][ext][query]'
},
mode: '', //development, production(默认) 或 none
resolve: {
extensions: [".js", ".vue", "..."],
alias: {
'@': resolve('./src'),
'vue$': 'vue/dist/vue.esm.js'
}
},
externals: [],
resolveLoader: {
modules: [
path.join(baseDir, "node_modules"),
path.join(__dirname, "../node_modules")
]
},
module: {
rules: []
},
optimization: {},
plugins: []
}

rules.js

"use strict";

module.exports = [
{
test: /\.css$/,
use: [
// mini-css-extract-plugin 和 style-loader 是一样的功能(将JS字符串嵌入<style>标签内),不可以同时使用。
// "style-loader",
"css-loader",
"postcss-loader"
]
},
{
test: /\.sass$/,
use: [
// "style-loader",
"css-loader",
"sass-loader",
"postcss-loader"
]
},
{
test: /\.less$/,
use: [
// "style-loader",
"css-loader",
"less-loader",
"postcss-loader"
]
},
{
test: /\.stylus/,
use: [{
loader: "css-loader",
options: {
sourceMap: false
}
}, {
loader: "stylus-loader",
options: {
sourceMap: false
}
}, {
loader: "postcss-loader"
}]
},
{
test: /\.ts$/,
exclude: [/node_modules/],
use: [
"ts-loader"
]
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.html$/,
use: [
"html-loader"
]
},
{
test: /\.(jsx?|ts)$/,
exclude: [/node_modules/],
use: [
"eslint-loader"
],
enforce: "pre"
},
{
test: /\.(png|jpg|gif|jpeg|svg|ttf|eot|woff2?|oft)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000
}
}
}
]

plugins.js

"use strict";

const path = require('path')
const webpack = require('webpack')

/**
* 获取当前env
* @returns env 环境
*/
function getEnv() {
const NODE_ENV = process.env.NODE_ENV
? process.env.NODE_ENV
: this.env ? this.env : "development"
return JSON.stringify(NODE_ENV)
}

/**
* 配置全局变量
*/
exports.define = {
name: webpack.DefinePlugin,
args() {
const NODE_ENV = getEnv.call(this)
return {
"process.env.NODE_ENV": NODE_ENV
}
}
}

/**
* 创建html
*/
exports.html = {
name: 'html-webpack-plugin',
args() {
const NODE_ENV = getEnv.call(this)
return {
title: 'test-service',
minify: NODE_ENV === 'production',
template: path.resolve(
this.baseDir,
'./src/index.html'
)
}
}
}

/**
* vue-loader webpack5需要单独vue-loader-plugin
*/
exports.vue = {
name: 'vue-loader',
entry: 'VueLoaderPlugin'
}

/**
* CSS 提取到单独的文件中
*/
exports.mincss = {
env: ['prod'],
name: 'mini-css-extract-plugin',
args() {
return {
filename: '[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css',
ignoreOrder: true
}
}
}

/**
* 复制文件
*/
exports.copy = {
name: 'copy-webpack-plugin',
args() {
return {
patterns:[{
from: path.resolve(this.baseDir, './src/static'),
to: path.resolve(this.baseDir, './dist')
}]
}
}
}

/**
* 可视化输出文件的大小
*/
exports.analyzer = {
env: ['prod'],
name: 'webpack-bundle-analyzer',
entry: 'BundleAnalyzerPlugin'
}

/**
* 压缩css
*/
exports.cssminimizer = {
env: ['prod'],
name: 'css-minimizer-webpack-plugin'
}
合并webpack配置(test.config.js和config目录下的webpack.js合并)

​webpack-merge​

webpack 配置项合并
​npm gitwebpack-merge​

设置rules

合并test.config.rules.js合并

baseConfig.js

setRules(rules) {
if(rules){
this.rules = utils.unionBy(this.rules, rules, 'test')
}

const target = _.cloneDeep(this.rules)

const cssExtension = ['css','wxss','less','postcss','sass','scss','stylus','styl']
const pattern=/[\\\/.$]/g;

target.map((item)=>{
const name = item.test.toString().replace(pattern, '')
if(cssExtension.includes(name)){
if(this.env === 'production'){
item.use.unshift({
loader: MiniCssExtractPlugin.loader,
options: {
esModule: false
}
})
}else{
item.use.unshift('style-loader')
}

}
})

this.webpackConfig.module.rules = target
}
设置plugins
setPlugins(plugins){
let target = _.cloneDeep(this.plugins)

let webpackPlugins = []

Object.keys(target).forEach(name => {
let itemPlugin = target[name]

if(this.isEnv(itemPlugin.env)){

let plugin, pluginName

if (_.isString(itemPlugin.name) || _.isFunction(itemPlugin.name)) {
let Clazz = itemPlugin.name
if (_.isString(itemPlugin.name)) {
pluginName = itemPlugin.name;
Clazz = require(itemPlugin.name)
} else if (_.isFunction(itemPlugin.name)) {
pluginName = itemPlugin.name.name
}

if (itemPlugin.entry) {
Clazz = Clazz[itemPlugin.entry]
}

if (itemPlugin.args) {
let args;
if (_.isFunction(itemPlugin.args)) {
args = itemPlugin.args.apply(this)
} else {
args = itemPlugin.args
}
plugin = new (Function.prototype.bind.apply(Clazz, [null].concat(args)))
} else {
plugin = new Clazz()
}
}

if (plugin) {
plugin.__plugin__ = pluginName
plugin.__lable__ = name
webpackPlugins.push(plugin)
}
}
})

let newPlugins = webpackPlugins
if(plugins){
// 基础plugins配置 合并 test.config.js plugins
let mergePlugins = [...plugins, ...webpackPlugins]
let hash = {}
newPlugins = mergePlugins.reduce((item, next) => {
hash[next.constructor.name] ? '' : hash[next.constructor.name] = true && item.push(next)
return item
}, [])
}

this.webpackConfig.plugins = newPlugins
}

dev环境启动serve

webpack-dev-server

创建本地服务器 ​​npm webpack-dev-server​

devConfig.js

"use strict";

const webpackDevServer = require("webpack-dev-server")
const BaseConfig = require('./BaseConfig')

class DevConfg extends BaseConfig {
constructor(options){
super(options)

this.options = options

this.init()
}

init(){
this.options.mode = 'development'

if(this.options.dev){
this.webpackConfig.output = this.options.dev
}

this.initBase(this.options)

}

run(){
this.createDevServer()
}

/**
* 创建 devServer
*/
createDevServer(){
logger.info('start server...')

const devServer = this.mergeDevServerConfig()

const compiler = this.webpack(this.webpackConfig)
const server = new webpackDevServer(compiler, devServer)

server.listen(this.port, '0.0.0.0', err => {
if(err){
console.log(err)
}
})

const sigs = ["SIGINT", "SIGTERM"]
sigs.forEach(function(sig) {
process.on(sig, function() {
server.close()
process.exit()
});
});
}
/**
* 合并devServer配置
*/
mergeDevServerConfig() {
let devServer = {
hot: true,
open: true,
disableHostCheck: true
}
if(this.webpackConfig.devServer){
devServer = Object.assign({}, devServer, this.webpackConfig.devServer)
}

return devServer
}
}

module.exports = DevConfg

prod打包及测试环境打包

"use strict";

const webpack = require('webpack')
const BaseConfig = require('./BaseConfig')

class ProdConfg extends BaseConfig {
constructor(options){
super(options)

this.options = options

this.init()
}

init(){
this.options.mode = 'production'
this.options.devtool = 'source-map'

if(this.options.build && !this.options.test){
this.webpackConfig.output = this.options.build
}
if(this.options.test && this.options.dev){
this.webpackConfig.output = this.options.dev
}

// webpack5 已内置该插件
// this.webpackConfig.optimization.minimize = true
// this.webpackConfig.optimization.minimizer = [new TerserPlugin()]

this.initBase(this.options)
}

run() {
this.builder()
}

/**
* prod 打包
*/
builder(){
webpack(this.webpackConfig, (err, stats) => {
if (err) console.log(err)
if (stats.hasErrors()) {
console.log(new Error(`Build failed with errors.', ${stats.toString({colors: true})}`))
}
})
}
}

module.exports = ProdConfg

cli-service 完成


test-project 中的package.json中要引入test-service 包

直接把test-cli和cli-service包发布到npm ​​npm publish​

已上为部分代码,仅供参考