webpack 构建前端项目(3)

569 阅读4分钟

上篇介绍了打包和构建dev服务相关的内容,这篇介绍src相关的构造。

src文件目录结构


  • src/modules存放各个模块;
  • src/store存放公共的vuex;
  •  common文件夹存放公共的文件;
  • component文件夹存放组件文件。

模块内部 以home为例,

  • home/main.js是入口文件,
  • home/router下为vue-router文件
  •  home/store为vuex文件,
  • views为页面文件夹

命令行新建/删除文件

采用命令行新建和删除文件,可以保证为保证各个模块之间、各个文件之间的目录结构风格的统一,方便项目的修改与维护。

在template文件夹下新建init文件夹、router文件夹、views文件夹,分别作为模块初始化、router添加、页面添加的模板文件。

init文件夹下有 main.js,app.vue,router文件夹 store文件夹作为模块的初始文件。


安装 copy-template-dir 用于复制文件,安装 rimraf用于删除文件,安装inquirer用于获取用户命令行输入。

npm i copy-template-dir rimraf inquirer -D

在build文件夹下新建一个init-page.js文件

创建时,如果模块不存在,测将 template下的init文件夹复制进模块文件夹,然后将tempalte下views文件夹复制进建立的模块下的views文件夹下


const chalk=require('chalk')
const fs=require('fs')
const copy=require('copy-template-dir')
const rm=require('rimraf')
const pathParam=require('minimist')(process.argv.slice(2))
const args=pathParam.path  //传入的参数 如 home/index
const isDelete=pathParam.d==true  //是否是 delete-page命令
const {prompt}=require('inquirer')
const path=require('path')
const res=p=>path.join(process.cwd(),p)

//异常处理
let err={msg:''}
Object.defineProperty(err,"msg",{
  set(val){
    if(val){
      console.log(chalk.red(val))
      process.exit(1)
    }
  }})

err.msg=args?'':'please input a router'  //验证输入非空
err.msg=/^((\w|-)+)(\/)((\w|-|\/)+)?((\w|-)+)$/.test(args)?'':'input a useful param, not include "*、?、&" and so on' //特殊字符验证
const mod=args.split('/')[0]  //模块名
const pat=args.split('/').slice(1).join('/')  //路径

fs.stat(res(`src/modules/${mod}/router/${pat}`),(er)=>{ //判断文件是否已存在
  if(!er&&!isDelete){
      err.msg='file already existed,no create again '
  }else if(er&&!isDelete){
    createProject()
  }else if(isDelete){
    deleteProject()
  }})

const createProject=()=>{  //新建
  const exist=fs.existsSync(res(`src/modules/${mod}`))
    if(!exist){
      copy(res('template/init'),res(`src/modules/${mod}`),er=>{ //创建模块
        err.msg=er?'fail to create module,please checkout':''
        console.log('copy module')
        processProject()  //创建内容
      })
    }else{
      processProject() //创建内容
    }}

const processProject=()=>{
  prompt([ //查询用户输入
    {
      type:'input',
      message:'please input a title',
      name:'title'
    }
  ]).then(r=>{
      console.log(r)
    copy(res('template/views'),res(`src/modules/${mod}/views/${pat}`),er=>{ //复制页面文件.vue
      err.msg=er?'fail to copy views,please checkout':''
      console.log('copy views')
    })
    copy(res('template/router'),res(`src/modules/${mod}/router/${pat}`),{path:pat,module:mod,title:r.title},er=>{  //复制路由文件
      err.msg=er?'fail to copy router,please checkout':''
            console.log('copy router')
    })
    const str= fs.readFileSync(res(`src/modules/${mod}/router/index.js`)) //读取路由文件
    let str2=str.toString()
    let patName=pat.replace(/\//g,'_') //将路由'/'转成'_'
    str2=str2.replace(/(Vue.use\(router\))/,`$1\nimport ${patName} from"\.\/${pat}/index.js"`).replace(/(routes:\[)/,`$1\n    \.\.\.${patName},`) //替换文件
    fs.writeFileSync(res(`src/modules/${mod}/router/index.js`),str2)  //写入文件
  })
}

const deleteProject=()=>{  //删除
  let patName=pat.replace(/\//g,'_')
  rm(res(`src/modules/${mod}/views/${pat}`),err=>{ //删除页面文件
    console.log('delete view')
  })
  rm(res(`src/modules/${mod}/router/${pat}`),err=>{ //删除 路由文件
    console.log('delete router')
  })
  const str= fs.readFileSync(res(`src/modules/${mod}/router/index.js`)) //读取主路由文件
  let str2=str.toString()
  let reg1=new RegExp(`\nimport ${patName} from"\.\/${pat}\/index\.js"`)
  let reg2=new RegExp(`\n    \.\.\.${patName},`)
  str2=str2.replace(reg1,'').replace(reg2,'') //修改主路由文件
  console.log(str2)
  fs.writeFileSync(res(`src/modules/${mod}/router/index.js`),str2) //写入主路由文件
}

在package.json scripts中加入

"init-page": "node build/init-page --path",
"delete-page": "node build/init-page -d  --path",

在命令行输入 "npm run init-page 模块名/路径" 、"npm run delete-page 模块名/路径"即可新增、删除文件。

init-page

创建时,以  "npm run init-page home/detail" 命令为例,

  • 先进行输入验证,通过后
  • 如果home模块不存在,则将 template下的init文件夹复制进 "src/modules/home" 文件夹;
  • 命令行查询 输入页面标题;
  • 将tempalte下views文件夹复制进 "src/modules/home/views/detail" 文件夹下;
  • 再将template下router文件夹复制进  "src/modules/router/detail" 文件之下,并通过copy-template-dir 注入页面标题、路径及组件路径;

copy(res('template/router'),res(`src/modules/${mod}/router/${pat}`),{path:pat,module:mod,title:r.title},er=>{  //复制路由文件
      err.msg=er?'fail to copy router,please checkout':''
            console.log('copy router')
    })

//template/router/index.js文件:

export default [{
  path:'/{{path}}',
  component:()=>import('@/modules/{{module}}/views/{{path}}/index.vue'),
  meta:{
    title:'{{title}}'
  }
}]

  • 更新 "src/modules/home/router/index.js" 文件;

delete-page

删除时 以 “npm run delete-page home/detail”命令为例,先进行输入验证,通过后

  • 删除 "src/modules/home/views/detail"文件夹
  • 删除 “ src/modules/home/router/detail”文件夹
  • 更新 “src/modules/home/router/index.js”文件


自动注册基础组件

项目的基础组件放置 src/component/base文件夹下

在src/common/js文件夹下新建一个registerComponent.js 文件,用于自动注册基础组件

import Vue from "vue"
const files = require.context("@component/base", true, /index\.vue$/) //扫描基础组件目录
files.keys().map(files).map(item => { //生成 require对象数组
  const name = item.default.__file.replace(/^(.+\/)((\w|-|_)+)(\/index.vue)$/, "$2").replace(/(\w)/,(v)=>v.toUpperCase()) //获取组件名字并大写首字母
  Vue.component(`v${name}`, item.default) //注册组件
})

开启mock服务

在package.json scripts中加入

"dev:mock": "set NODE_ENV='development' && node build/dev.js --open --mock  --module",

在param.js中加入

const param=require('minimist')(process.argv.slice(2))

mock:param.mock?true:false

在prod.conf.js文件中加入 

new webpack.DefinePlugin({
      MOCK:mock,
      IS_DEV:true
})

在项目根目录新建mock文件夹作为mock目录,

项目的请求方法中直接用require获取mock数据

 request (o) {
    const _this = this.vm
    return new Promise((resolve, reject) => {
      if (MOCK) { //判断是否启用mock
        try {
          const data = require(`@mock/${o.url.replace(/\//g, "_")}.js`) //获取mock数据          _this.$load.open()          setTimeout(() => {            _this.$load.hide()            resolve(data)          }, 500)        } catch (err) {          reject({            errCode: 1,            msg: "404 wrong request"          })        }      } else {
        ....
      }catach(err){}
    })
  },

在项目生产打包时,并不需要将mock文件打包,需在build/prod.conf.js中设置 uglifyis-webpack-plugin

const uglifyJs=require('uglifyjs-webpack-plugin')

  optimization:{
    minimizer:[
      new uglifyJs({
        exclude:/\/mock/,  //忽略mock文件夹的打包
        uglifyOptions:{
          compress:{
            drop_debugger:true,
            drop_console:true
          }
        }
      })
    ],
}



在 build/dev.js 中 启用 mock静态资源服务器

const {mock,moduleName,openB}=require('./param')

const res=p=>path.join(process.cwd(),p)

if(mock){
  app.use(express.static(res('mock/assets')))
}


多模块启动

应用node.js子进程进行多个模块的启动和打包

新建build/server.js文件

const {moduleName,mock}=require('./param')
const {exec}=require('child_process')
const port=require('../config/local.js').router  //本地存放的端口号
const chalk=require('chalk')

//错误处理
let err={msg:''}
Object.defineProperty(err,'msg',{
  set(val){
    if(val!==''){
      console.log(chalk.red(val))
      process.exit()
    }
  }})

err.msg=moduleName?'':'please input a module name'let line=mock?"set NODE_ENV=development && node build/dev.js --mock --module":"set NODE_ENV=development && node build/dev.js --module"//本地配置的模块名
let modules=Object.keys(port).reduce((ret,item)=>{  //生成配租文件 端口数组
  ret.push(item)
  return ret
},[])

//遍历并过滤输入的模块
moduleName.split(',').filter(item=>{ 
 let ret= modules.indexOf(item)>-1
  if(!ret){ //未配置端口号
    console.log(chalk.yellow(`${item} module not found port config;please check in /config/local.js \n`))
  }
  return ret
}).map(item=>{  // 启动子进程
  exec(`${line} ${item}`,(error,stdout,stdErr)=>{
    console.log(stdErr)
    error && console.log(error)
  })
  console.log(chalk.green(`${item} running  at  http://localhost:${port[item]}/${item}.html`))
})

在package.json scripts加入

"server": "set NODE_ENV='development' && node build/server.js  --module",
"server:mock": "set NODE_ENV='development' && node build/server.js --mock  --module",

在命令行 输入 "npm run server home,car"  即可启动 home,car两个模块

本地模块间的页面跳转

获取本地服务端口配置,

const { router } = require(IS_DEV ? "../../../config/local.js" : "../../../config/prod.js")

页面跳转方法

 go (param) {
    let { url, data } = param
    let r = ""
    if (data) {
      Object.keys(data).map(item => { //处理页面传参
        r += `&${item}=${data[item]}`
      })
      r = r.slice(1)
    }
    url = url.replace(/(\.html)/, `$1?${r}`) //注入页面传参
    const m = url.split(".")[0] 
    location.href = window.encodeURI(`http://localhost:${router[m]}/${url}`)  },

如跳转到 home.html/#/index 页

App.go(
  {
    url:'home.html/#/index',
    data:{
        id:1           
    }
  }
)

全部打包

安装 glob 用于扫描目录文件

npm i glob -D

在build文件夹下新建一个buildAll.js文件

const glob=require('glob')
const {exec}=require('child_process')
const path=require('path')
const chalk=require('chalk')

glob(path.join(process.cwd(),'/src/modules/*/main.js'),(err,files)=>{ //扫描指定目录下的入口文件  
console.log(files)  
files.map(item=>{
    let name=item.replace(/(\/main.js)$/,'')
    let n=name.slice(name.lastIndexOf('/')+1)  //解析出模块名
    console.log(n)
    exec(`set NODE_ENV=production && node build/build.js --module ${n}`,(error,stdout,stdErr)=>{ //调起子进程
      console.log(stdout,stdErr) 
     if(error){
        console.log(error)
        process.exit()
      }
    })
  })
})

在package.json scripts中加入

"buildAll": "node build/buildAll.js"

命令行输入 npm run buildAll 就可全部打包了

github地址


github.com/nicole11223…